---
phase: 01-foundation-setup
plan: 02
type: execute
wave: 2
depends_on:
  - 01-01
files_modified:
  - src/clawrium/core/deps.py
  - src/clawrium/cli/init.py
  - src/clawrium/cli/main.py
  - tests/test_deps.py
  - tests/test_cli_init.py
autonomous: true
requirements:
  - INIT-02

must_haves:
  truths:
    - "User sees clear status of all dependencies (Python, Ansible, ansible-runner)"
    - "User sees OK/MISSING status for each dependency"
    - "User receives actionable install instructions for any missing dependencies"
    - "clm init exits with code 1 if any dependency is missing"
  artifacts:
    - path: "src/clawrium/core/deps.py"
      provides: "Dependency detection functions"
      exports: ["DependencyStatus", "check_python", "check_ansible", "check_ansible_runner", "check_all_dependencies"]
    - path: "tests/test_deps.py"
      provides: "Tests for dependency detection"
      min_lines: 60
  key_links:
    - from: "src/clawrium/cli/init.py"
      to: "src/clawrium/core/deps.py"
      via: "import check_all_dependencies"
      pattern: "from clawrium\\.core\\.deps import"
    - from: "src/clawrium/cli/init.py"
      to: "rich.table"
      via: "table output"
      pattern: "from rich\\.table import Table"
---

<objective>
Implement dependency detection for `clm init` that checks Python, Ansible, and ansible-runner, displaying status in a table with actionable install instructions for missing dependencies.

Purpose: Users need to know their system is ready before using Clawrium. Clear feedback about missing dependencies and how to install them prevents frustration and support requests.

Output: Enhanced `clm init` that shows dependency status table, with tests for detection logic.
</objective>

<execution_context>
@/home/devashish/workspace/ric03uec/clawrium/.claude/get-shit-done/workflows/execute-plan.md
@/home/devashish/workspace/ric03uec/clawrium/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation-setup/01-RESEARCH.md

<interfaces>
<!-- From Plan 01 - files that must exist before this plan executes -->

From src/clawrium/core/config.py (created in Plan 01):
```python
def get_config_dir() -> Path:
    """Get the Clawrium configuration directory path."""
    ...

def init_config_dir() -> Path:
    """Create and return the configuration directory."""
    ...
```

From src/clawrium/cli/main.py (created in Plan 01):
```python
app = typer.Typer(
    name="clm",
    help="Clawrium - Manage your AI assistant fleet",
    no_args_is_help=True,
)

@app.command()
def init() -> None:
    """Initialize Clawrium and check dependencies."""
    init_command()
```

From src/clawrium/cli/init.py (created in Plan 01):
```python
def init() -> None:
    """Initialize Clawrium configuration directory."""
    config_dir = init_config_dir()
    console.print(f"[green]Clawrium initialized![/green]")
    console.print(f"Config directory: {config_dir}")
```

From research - Dependency detection pattern:
```python
@dataclass
class DependencyStatus:
    name: str
    found: bool
    version: str | None
    path: str | None
    install_hint: str

def check_ansible() -> DependencyStatus:
    path = shutil.which("ansible")
    if not path:
        return DependencyStatus(
            name="ansible",
            found=False,
            version=None,
            path=None,
            install_hint="Install via: pipx install ansible"
        )
    # ... version parsing
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Implement dependency detection module</name>
  <files>
    src/clawrium/core/deps.py,
    tests/test_deps.py
  </files>
  <read_first>
    /home/devashish/workspace/ric03uec/clawrium/.planning/phases/01-foundation-setup/01-RESEARCH.md (Pattern 3: Dependency Detection, Pitfall 4 and 5),
    /home/devashish/workspace/ric03uec/clawrium/tests/conftest.py (fixtures)
  </read_first>
  <behavior>
    - Test: DependencyStatus dataclass has name, found, version, path, install_hint fields
    - Test: check_python() returns found=True with version
    - Test: check_ansible() returns found=True when ansible binary exists
    - Test: check_ansible() returns found=False with install_hint when missing
    - Test: check_ansible_runner() returns found=True when importable
    - Test: check_ansible_runner() returns found=False when not importable
    - Test: check_all_dependencies() returns list of 3 DependencyStatus objects
    - Test: install hints recommend pipx for ansible (Ubuntu externally-managed-environment)
  </behavior>
  <action>
1. Create `tests/test_deps.py` (RED - tests first):
```python
"""Tests for dependency detection."""

from pathlib import Path
from unittest.mock import patch

import pytest

from clawrium.core.deps import (
    DependencyStatus,
    check_ansible,
    check_ansible_runner,
    check_all_dependencies,
    check_python,
)


class TestDependencyStatus:
    """Tests for DependencyStatus dataclass."""

    def test_has_required_fields(self) -> None:
        """DependencyStatus should have all required fields."""
        status = DependencyStatus(
            name="test",
            found=True,
            version="1.0.0",
            path="/usr/bin/test",
            install_hint="",
        )
        assert status.name == "test"
        assert status.found is True
        assert status.version == "1.0.0"
        assert status.path == "/usr/bin/test"
        assert status.install_hint == ""


class TestCheckPython:
    """Tests for check_python function."""

    def test_python_always_found(self) -> None:
        """Python should always be found (we're running in it)."""
        result = check_python()
        assert result.found is True
        assert result.name == "python"

    def test_python_has_version(self) -> None:
        """Python check should include version."""
        result = check_python()
        assert result.version is not None
        assert "3." in result.version  # Python 3.x


class TestCheckAnsible:
    """Tests for check_ansible function."""

    def test_ansible_found_when_exists(self) -> None:
        """Should return found=True when ansible binary exists."""
        with patch("shutil.which") as mock_which:
            mock_which.return_value = "/usr/bin/ansible"
            with patch("subprocess.run") as mock_run:
                mock_run.return_value.returncode = 0
                mock_run.return_value.stdout = "ansible [core 2.15.0]"

                result = check_ansible()

                assert result.found is True
                assert result.name == "ansible"
                assert result.path == "/usr/bin/ansible"

    def test_ansible_missing_when_not_exists(self) -> None:
        """Should return found=False when ansible binary not found."""
        with patch("shutil.which") as mock_which:
            mock_which.return_value = None

            result = check_ansible()

            assert result.found is False
            assert result.name == "ansible"
            assert result.path is None

    def test_ansible_install_hint_recommends_pipx(self) -> None:
        """Install hint should recommend pipx for Ubuntu compatibility."""
        with patch("shutil.which") as mock_which:
            mock_which.return_value = None

            result = check_ansible()

            assert "pipx" in result.install_hint.lower()


class TestCheckAnsibleRunner:
    """Tests for check_ansible_runner function."""

    def test_ansible_runner_found_when_importable(self) -> None:
        """Should return found=True when ansible_runner is importable."""
        # ansible_runner is a project dependency, so it should be importable
        result = check_ansible_runner()
        assert result.found is True
        assert result.name == "ansible-runner"

    def test_ansible_runner_missing_when_not_importable(self) -> None:
        """Should return found=False when import fails."""
        with patch.dict("sys.modules", {"ansible_runner": None}):
            with patch("importlib.util.find_spec") as mock_find:
                mock_find.return_value = None

                result = check_ansible_runner()

                assert result.found is False
                assert "uv add" in result.install_hint or "pip install" in result.install_hint


class TestCheckAllDependencies:
    """Tests for check_all_dependencies function."""

    def test_returns_list_of_three(self) -> None:
        """Should return status for python, ansible, and ansible-runner."""
        result = check_all_dependencies()
        assert len(result) == 3
        names = [dep.name for dep in result]
        assert "python" in names
        assert "ansible" in names
        assert "ansible-runner" in names

    def test_all_are_dependency_status(self) -> None:
        """All items should be DependencyStatus objects."""
        result = check_all_dependencies()
        for dep in result:
            assert isinstance(dep, DependencyStatus)
```

2. Run tests to confirm RED:
```bash
uv run pytest tests/test_deps.py -x
```

3. Create `src/clawrium/core/deps.py` (GREEN):
```python
"""Dependency detection for Clawrium."""

import importlib.util
import shutil
import subprocess
import sys
from dataclasses import dataclass

__all__ = [
    "DependencyStatus",
    "check_python",
    "check_ansible",
    "check_ansible_runner",
    "check_all_dependencies",
]


@dataclass
class DependencyStatus:
    """Status of a dependency check.

    Attributes:
        name: Human-readable dependency name.
        found: Whether the dependency is available.
        version: Version string if found, None otherwise.
        path: Path to binary/module if found, None otherwise.
        install_hint: Instructions for installing if missing.
    """

    name: str
    found: bool
    version: str | None
    path: str | None
    install_hint: str


def check_python() -> DependencyStatus:
    """Check Python availability and version.

    Returns:
        DependencyStatus for Python (always found since we're running in it).
    """
    version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
    return DependencyStatus(
        name="python",
        found=True,
        version=version,
        path=sys.executable,
        install_hint="",
    )


def check_ansible() -> DependencyStatus:
    """Check if Ansible is installed and get its version.

    Returns:
        DependencyStatus for Ansible.
    """
    path = shutil.which("ansible")
    if not path:
        return DependencyStatus(
            name="ansible",
            found=False,
            version=None,
            path=None,
            install_hint="Install via: pipx install ansible (recommended) or sudo apt install ansible",
        )

    # Get version
    try:
        result = subprocess.run(
            ["ansible", "--version"],
            capture_output=True,
            text=True,
            timeout=10,
        )
        # First line is like: "ansible [core 2.15.0]"
        version = result.stdout.split("\n")[0] if result.returncode == 0 else None
    except (subprocess.TimeoutExpired, OSError):
        version = None

    return DependencyStatus(
        name="ansible",
        found=True,
        version=version,
        path=path,
        install_hint="",
    )


def check_ansible_runner() -> DependencyStatus:
    """Check if ansible-runner Python package is available.

    Returns:
        DependencyStatus for ansible-runner.
    """
    spec = importlib.util.find_spec("ansible_runner")
    if spec is None:
        return DependencyStatus(
            name="ansible-runner",
            found=False,
            version=None,
            path=None,
            install_hint="Install via: uv add ansible-runner (or pip install ansible-runner)",
        )

    # Get version if available
    try:
        import ansible_runner

        version = getattr(ansible_runner, "__version__", None)
        path = str(spec.origin) if spec.origin else None
    except ImportError:
        version = None
        path = None

    return DependencyStatus(
        name="ansible-runner",
        found=True,
        version=version,
        path=path,
        install_hint="",
    )


def check_all_dependencies() -> list[DependencyStatus]:
    """Check all required dependencies.

    Returns:
        List of DependencyStatus for python, ansible, and ansible-runner.
    """
    return [
        check_python(),
        check_ansible(),
        check_ansible_runner(),
    ]
```

4. Run tests to confirm GREEN:
```bash
uv run pytest tests/test_deps.py -v
```
  </action>
  <verify>
    <automated>cd /home/devashish/workspace/ric03uec/clawrium && uv run pytest tests/test_deps.py -v</automated>
  </verify>
  <acceptance_criteria>
    - src/clawrium/core/deps.py contains `@dataclass`
    - src/clawrium/core/deps.py contains `class DependencyStatus:`
    - src/clawrium/core/deps.py contains `def check_python() -> DependencyStatus:`
    - src/clawrium/core/deps.py contains `def check_ansible() -> DependencyStatus:`
    - src/clawrium/core/deps.py contains `def check_ansible_runner() -> DependencyStatus:`
    - src/clawrium/core/deps.py contains `def check_all_dependencies() -> list[DependencyStatus]:`
    - src/clawrium/core/deps.py contains `shutil.which("ansible")`
    - src/clawrium/core/deps.py contains `pipx install ansible`
    - tests/test_deps.py contains `class TestDependencyStatus`
    - tests/test_deps.py contains `class TestCheckAnsible`
    - `uv run pytest tests/test_deps.py -v` shows all tests passing
  </acceptance_criteria>
  <done>Dependency detection module implemented with check functions for Python, Ansible, and ansible-runner, all tests pass</done>
</task>

<task type="auto">
  <name>Task 2: Integrate dependency check into clm init</name>
  <files>
    src/clawrium/cli/init.py,
    tests/test_cli_init.py
  </files>
  <read_first>
    /home/devashish/workspace/ric03uec/clawrium/src/clawrium/cli/init.py (current implementation to extend),
    /home/devashish/workspace/ric03uec/clawrium/src/clawrium/core/deps.py (functions to import),
    /home/devashish/workspace/ric03uec/clawrium/.planning/phases/01-foundation-setup/01-RESEARCH.md (Main CLI Entry Point example with table output),
    /home/devashish/workspace/ric03uec/clawrium/tests/test_cli_init.py (existing tests to extend)
  </read_first>
  <action>
1. Update `src/clawrium/cli/init.py` to add dependency table:
```python
"""Init command for Clawrium."""

import typer
from rich.console import Console
from rich.table import Table

from clawrium.core.config import init_config_dir
from clawrium.core.deps import check_all_dependencies

__all__ = ["init"]

console = Console()


def init() -> None:
    """Initialize Clawrium configuration directory and check dependencies.

    Creates the configuration directory at ~/.config/clawrium/
    (or XDG_CONFIG_HOME/clawrium/ if set) and verifies that all
    required dependencies are available.

    Exits with code 1 if any dependency is missing.
    """
    # Create config directory
    config_dir = init_config_dir()
    console.print(f"[green]Clawrium initialized![/green]")
    console.print(f"Config directory: {config_dir}")
    console.print()

    # Check dependencies
    deps = check_all_dependencies()

    table = Table(title="Dependency Status")
    table.add_column("Dependency", style="cyan")
    table.add_column("Status")
    table.add_column("Version/Path")
    table.add_column("Action Required")

    all_found = True
    for dep in deps:
        if dep.found:
            status = "[green]OK[/green]"
        else:
            status = "[red]MISSING[/red]"
            all_found = False

        version_or_path = dep.version or dep.path or "-"
        action = dep.install_hint if not dep.found else "-"
        table.add_row(dep.name, status, version_or_path, action)

    console.print(table)

    if not all_found:
        console.print()
        console.print("[yellow]Some dependencies are missing. Please install them before continuing.[/yellow]")
        raise typer.Exit(code=1)
```

2. Add dependency-related tests to `tests/test_cli_init.py`:

Read the existing file first, then append these tests to the TestCliInit class:

```python
    def test_init_shows_dependency_table(self, isolated_config: Path) -> None:
        """clm init should show dependency status table."""
        result = runner.invoke(app, ["init"])

        assert "Dependency Status" in result.output
        assert "python" in result.output.lower()
        assert "ansible" in result.output.lower()

    def test_init_shows_ok_for_found_deps(self, isolated_config: Path) -> None:
        """clm init should show OK for found dependencies."""
        result = runner.invoke(app, ["init"])

        # Python and ansible-runner should always be found (project deps)
        assert "OK" in result.output

    def test_init_exits_1_when_deps_missing(
        self, isolated_config: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """clm init should exit 1 if any dependency is missing."""
        from clawrium.core import deps

        # Mock ansible as missing
        original_check = deps.check_ansible

        def mock_check_ansible():
            return deps.DependencyStatus(
                name="ansible",
                found=False,
                version=None,
                path=None,
                install_hint="Install via: pipx install ansible",
            )

        monkeypatch.setattr(deps, "check_ansible", mock_check_ansible)

        result = runner.invoke(app, ["init"])

        assert result.exit_code == 1
        assert "MISSING" in result.output

    def test_init_shows_install_hint_for_missing(
        self, isolated_config: Path, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """clm init should show install instructions for missing deps."""
        from clawrium.core import deps

        def mock_check_ansible():
            return deps.DependencyStatus(
                name="ansible",
                found=False,
                version=None,
                path=None,
                install_hint="Install via: pipx install ansible",
            )

        monkeypatch.setattr(deps, "check_ansible", mock_check_ansible)

        result = runner.invoke(app, ["init"])

        assert "pipx install ansible" in result.output
```

3. Also add the missing import at the top of test_cli_init.py if not present:
```python
import pytest
```

4. Run all CLI tests:
```bash
uv run pytest tests/test_cli_init.py -v
```
  </action>
  <verify>
    <automated>cd /home/devashish/workspace/ric03uec/clawrium && uv run pytest tests/test_cli_init.py -v</automated>
  </verify>
  <acceptance_criteria>
    - src/clawrium/cli/init.py contains `from clawrium.core.deps import check_all_dependencies`
    - src/clawrium/cli/init.py contains `from rich.table import Table`
    - src/clawrium/cli/init.py contains `table = Table(title="Dependency Status")`
    - src/clawrium/cli/init.py contains `raise typer.Exit(code=1)`
    - src/clawrium/cli/init.py contains `[green]OK[/green]`
    - src/clawrium/cli/init.py contains `[red]MISSING[/red]`
    - tests/test_cli_init.py contains `test_init_shows_dependency_table`
    - tests/test_cli_init.py contains `test_init_exits_1_when_deps_missing`
    - `uv run pytest tests/test_cli_init.py -v` shows all tests passing
  </acceptance_criteria>
  <done>clm init shows dependency status table with OK/MISSING status and install hints, exits 1 if deps missing</done>
</task>

</tasks>

<verification>
After all tasks complete:

1. Full test suite passes:
```bash
uv run pytest tests/ -v --cov=clawrium
```

2. CLI shows dependency table:
```bash
uv run clm init
```

Expected output (if Ansible installed):
```
Clawrium initialized!
Config directory: /home/user/.config/clawrium

       Dependency Status
┏━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
┃ Dependency     ┃ Status ┃ Version/Path      ┃ Action Required ┃
┡━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
│ python         │ OK     │ 3.10.x            │ -               │
│ ansible        │ OK     │ ansible [core ... │ -               │
│ ansible-runner │ OK     │ 2.4.x             │ -               │
└────────────────┴────────┴───────────────────┴─────────────────┘
```

3. Exit code is correct:
```bash
uv run clm init; echo "Exit code: $?"
```
</verification>

<success_criteria>
- `clm init` shows "Dependency Status" table
- Table shows Python, Ansible, ansible-runner with OK/MISSING status
- Missing dependencies show actionable install instructions
- `clm init` exits with code 1 if any dependency is missing
- `clm init` exits with code 0 if all dependencies are present
- All tests in tests/test_deps.py and updated tests/test_cli_init.py pass
</success_criteria>

<output>
After completion, create `.planning/phases/01-foundation-setup/01-02-SUMMARY.md`
</output>
