Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ high-quality code.
supported)
- **Runtime**: Python 3.12+, Node.js 18+
- **Tools**: git, npm, uv ([astral.sh/uv](https://docs.astral.sh/uv/))
- **Recommended**: Rust/cargo ([rustup.rs](https://rustup.rs/)) — required for
the Rust recipe runner
- **Optional**: GitHub CLI (`gh`), Azure CLI (`az`)

Detailed setup:
Expand Down
17 changes: 16 additions & 1 deletion docs/PREREQUISITES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@ The amplihack framework requires the following tools. Each entry explains **what
| **uv** | latest | Fast Python package installer | Installs amplihack itself and its Python dependencies |
| **git** | 2.0+ | Version control | Branch management, PRs, and workflow automation |
| **claude** | latest | Claude Code CLI | Core AI coding assistant that amplihack extends |
| **cargo** | 1.70+ | Rust package manager | Installs the Rust recipe runner for fast recipe execution. Install via [rustup.rs](https://rustup.rs/) |

## Quick Check

Before installing amplihack, verify your prerequisites with this script:

```bash
# Copy-paste this into your terminal — no installation required
node --version && npm --version && uv --version && git --version && echo "All prerequisites OK"
node --version && npm --version && uv --version && git --version && cargo --version && echo "All prerequisites OK"
```

After installing amplihack, running `amplihack` will also check for missing tools and display installation instructions.
Expand Down Expand Up @@ -48,6 +49,9 @@ brew install uv

# git
brew install git

# Rust/cargo (for recipe runner)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

#### Verify Installation
Expand All @@ -57,6 +61,7 @@ node --version # Should show v18.x or higher
npm --version # Should show 9.x or higher
uv --version # Should show version info
git --version # Should show 2.x or higher
cargo --version # Should show 1.70 or higher
```

---
Expand All @@ -77,6 +82,9 @@ curl -LsSf https://astral.sh/uv/install.sh | sh

# git
sudo apt install git

# Rust/cargo (for recipe runner)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

#### Fedora/RHEL/CentOS
Expand All @@ -90,6 +98,9 @@ curl -LsSf https://astral.sh/uv/install.sh | sh

# git
sudo dnf install git

# Rust/cargo (for recipe runner)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```

#### Arch Linux
Expand All @@ -103,6 +114,9 @@ curl -LsSf https://astral.sh/uv/install.sh | sh

# git
sudo pacman -S git

# Rust/cargo (for recipe runner)
sudo pacman -S rust
```

#### Verify Installation
Expand All @@ -112,6 +126,7 @@ node --version # Should show v18.x or higher
npm --version # Should show 9.x or higher
uv --version # Should show version info
git --version # Should show 2.x or higher
cargo --version # Should show 1.70 or higher
```

---
Expand Down
172 changes: 106 additions & 66 deletions src/amplihack/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,63 +139,12 @@ def launch_command(args: argparse.Namespace, claude_args: list[str] | None = Non
# --subprocess-safe: skip all staging/env mutations to avoid concurrent
# write races when running as a delegate from another amplihack process
# (e.g. multitask workstreams). See issue #2567.
subprocess_safe = getattr(args, "subprocess_safe", False)

from .launcher.session_tracker import SessionTracker

# Detect nesting BEFORE any .claude/ operations
original_cwd = None
nesting_result = None

if not subprocess_safe:
from .launcher.auto_stager import AutoStager
from .launcher.nesting_detector import NestingDetector

detector = NestingDetector()
nesting_result = detector.detect_nesting(Path.cwd(), sys.argv)

# Auto-stage if nested execution in source repo detected
if nesting_result.requires_staging:
print("\n🚨 SELF-MODIFICATION PROTECTION ACTIVATED")
print(" Running nested in amplihack source repository")
print(" Auto-staging .claude/ to temp directory for safety")

stager = AutoStager()
original_cwd = Path.cwd()
staging_result = stager.stage_for_nested_execution(
original_cwd, f"nested-{os.getpid()}"
)

print(f" 📁 Staged to: {staging_result.temp_root}")
print(" Your original .claude/ files are protected")

# CRITICAL: Change to temp directory so all .claude/ operations happen there
os.chdir(staging_result.temp_root)
print(f" 📂 CWD changed to: {staging_result.temp_root}\n")

# Ensure amplihack framework is staged to ~/.amplihack/.claude/
_ensure_amplihack_staged()

# Auto-install missing SDK dependencies (e.g. agent-framework)
# Uses --python sys.executable to target the running interpreter,
# critical when launched via uvx (ephemeral venv != project .venv).
try:
from .dep_check import ensure_sdk_deps

dep_result = ensure_sdk_deps()
if not dep_result.all_ok:
logger.warning("Some SDK deps could not be installed: %s", dep_result.missing)
except Exception as e:
logger.debug("SDK dep check skipped: %s", e)

# Prompt to re-enable power-steering if disabled (#2544)
try:
from .power_steering.re_enable_prompt import prompt_re_enable_if_disabled

prompt_re_enable_if_disabled()
except Exception as e:
# Fail-open: log error but continue
logger.debug(f"Error checking power-steering re-enable prompt: {e}")
# Run shared startup (nesting, staging, deps, power-steering)
_common_launcher_startup(args)
nesting_result = getattr(args, "_nesting_result", None)

# Start session tracking
tracker = SessionTracker()
Expand Down Expand Up @@ -992,6 +941,101 @@ def _fix_global_statusline_path() -> None:
print(f"Warning: Could not update global statusline path: {e}")


def _ensure_rust_recipe_runner() -> None:
"""Ensure the Rust recipe runner binary is available.

Called during startup for all launcher paths. Non-fatal — logs a
warning if the binary cannot be installed or found.
"""
try:
from .recipes.rust_runner import ensure_rust_recipe_runner

if ensure_rust_recipe_runner():
print("✓ Rust recipe runner available")
else:
print("⚠ Rust recipe runner not installed — install Rust (rustup.rs) and run:")
print(" cargo install --git https://github.com/rysweet/amplihack-recipe-runner")
except Exception as e:
logging.getLogger(__name__).warning(
"Could not check recipe-runner-rs: %s", e, exc_info=True,
)


def _common_launcher_startup(args: "argparse.Namespace") -> None:
"""Run all shared startup initialization for launcher commands.

Consolidates initialization that must happen for every launcher path
(launch, claude, RustyClawd, copilot, codex, amplifier). Respects
--subprocess-safe to avoid concurrent write races (#2567).

Idempotent — safe to call multiple times (e.g. RustyClawd → launch_command).

Steps performed (in order):
1. Nesting detection and auto-staging
2. Framework staging (~/.amplihack/.claude/)
3. Rust recipe runner check
4. SDK dependency check
5. Power-steering re-enable prompt (#2544)
"""
# Idempotency guard — skip if already run this process
if getattr(args, "_startup_done", False):
return
args._startup_done = True # noqa: SLF001

subprocess_safe = getattr(args, "subprocess_safe", False)
if subprocess_safe:
return

# 1. Nesting detection — protect .claude/ when running in source repo
from .launcher.auto_stager import AutoStager
from .launcher.nesting_detector import NestingDetector

detector = NestingDetector()
nesting_result = detector.detect_nesting(Path.cwd(), sys.argv)

if nesting_result.requires_staging:
print("\n🚨 SELF-MODIFICATION PROTECTION ACTIVATED")
print(" Running nested in amplihack source repository")
print(" Auto-staging .claude/ to temp directory for safety")

stager = AutoStager()
staging_result = stager.stage_for_nested_execution(
Path.cwd(), f"nested-{os.getpid()}"
)

print(f" 📁 Staged to: {staging_result.temp_root}")
print(" Your original .claude/ files are protected")
os.chdir(staging_result.temp_root)
print(f" 📂 CWD changed to: {staging_result.temp_root}\n")

# Store nesting result on args for session tracking
args._nesting_result = nesting_result # noqa: SLF001

# 2. Framework staging
_ensure_amplihack_staged()

# 3. Rust recipe runner
_ensure_rust_recipe_runner()

# 4. SDK dependency check
try:
from .dep_check import ensure_sdk_deps

dep_result = ensure_sdk_deps()
if not dep_result.all_ok:
logger.warning("Some SDK deps could not be installed: %s", dep_result.missing)
except Exception as e:
logger.debug("SDK dep check skipped: %s", e)

# 5. Power-steering re-enable prompt (#2544)
try:
from .power_steering.re_enable_prompt import prompt_re_enable_if_disabled

prompt_re_enable_if_disabled()
except Exception as e:
logger.debug(f"Error checking power-steering re-enable prompt: {e}")


def _ensure_amplihack_staged() -> None:
"""Ensure .claude/ files are staged to ~/.amplihack/.claude/ for non-Claude commands.

Expand Down Expand Up @@ -1448,9 +1492,8 @@ def main(argv: list[str] | None = None) -> int:
if getattr(args, "append", None):
return handle_append_instruction(args)

# Ensure amplihack framework is staged (skip in subprocess-safe mode)
if not getattr(args, "subprocess_safe", False):
_ensure_amplihack_staged()
# Shared startup (nesting, staging, deps, power-steering)
_common_launcher_startup(args)

# Force RustyClawd usage (Rust implementation of Claude Code)
os.environ["AMPLIHACK_USE_RUSTYCLAWD"] = "1"
Expand All @@ -1474,9 +1517,8 @@ def main(argv: list[str] | None = None) -> int:
if getattr(args, "append", None):
return handle_append_instruction(args)

# Ensure amplihack framework is staged (skip in subprocess-safe mode)
if not getattr(args, "subprocess_safe", False):
_ensure_amplihack_staged()
# Shared startup (nesting, staging, deps, power-steering)
_common_launcher_startup(args)

# Handle auto mode
exit_code = handle_auto_mode("copilot", args, claude_args)
Expand All @@ -1498,9 +1540,8 @@ def main(argv: list[str] | None = None) -> int:
if getattr(args, "append", None):
return handle_append_instruction(args)

# Ensure amplihack framework is staged (skip in subprocess-safe mode)
if not getattr(args, "subprocess_safe", False):
_ensure_amplihack_staged()
# Shared startup (nesting, staging, deps, power-steering)
_common_launcher_startup(args)

# Handle auto mode
exit_code = handle_auto_mode("codex", args, claude_args)
Expand All @@ -1522,9 +1563,8 @@ def main(argv: list[str] | None = None) -> int:
if getattr(args, "append", None):
return handle_append_instruction(args)

# Ensure amplihack framework is staged (skip in subprocess-safe mode)
if not getattr(args, "subprocess_safe", False):
_ensure_amplihack_staged()
# Shared startup (nesting, staging, deps, power-steering)
_common_launcher_startup(args)

# Environment setup
if getattr(args, "no_reflection", False):
Expand Down
1 change: 1 addition & 0 deletions src/amplihack/launcher/copilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,7 @@ def launch_copilot(args: list[str] | None = None, interactive: bool = True) -> i
# Register awesome-copilot marketplace extensions (best-effort, silent on failure)
register_awesome_copilot_marketplace()


# Prompt to re-enable power-steering if disabled (#2544)
try:
from ..power_steering.re_enable_prompt import prompt_re_enable_if_disabled
Expand Down
1 change: 1 addition & 0 deletions tests/test_cli_claude_command_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ def _apply_patches(extra_patches: dict | None = None):
patches = {
"amplihack.cli.is_uvx_deployment": MagicMock(return_value=True),
"amplihack.cli.cleanup_legacy_skills": MagicMock(),
"amplihack.cli._common_launcher_startup": MagicMock(),
"amplihack.safety.GitConflictDetector": MagicMock(return_value=mock_detector),
"amplihack.safety.SafeCopyStrategy": MagicMock(return_value=mock_strategy_manager),
"amplihack.cli._configure_amplihack_marketplace": mock_configure,
Expand Down
Loading
Loading