---
phase: 02-host-management
plan: 03
type: execute
wave: 1
depends_on:
  - "02-01"
files_modified:
  - src/clawrium/core/hardware.py
autonomous: true
requirements:
  - HOST-05

must_haves:
  truths:
    - "Hardware capabilities are detected from remote hosts"
    - "Architecture, CPU cores, memory, disk space are extracted from Ansible facts"
    - "GPU presence and vendor are detected via lspci command"
    - "Hardware dict follows consistent schema for storage"
  artifacts:
    - path: "src/clawrium/core/hardware.py"
      provides: "Hardware detection via ansible-runner"
      exports: ["gather_hardware", "extract_hardware_from_facts", "parse_gpu_output"]
      min_lines: 60
  key_links:
    - from: "src/clawrium/core/hardware.py"
      to: "ansible_runner"
      via: "ansible_runner.run import"
      pattern: "import ansible_runner"
---

<objective>
Implement hardware capability detection via ansible-runner.

Purpose: Detect host hardware capabilities (architecture, CPU, memory, disk, GPU) for compatibility checking in Phase 3. Hardware is detected on `clm host add` and stored with the host record (per D-06: "Hardware detected on add").

Output: hardware.py module that uses ansible-runner to gather facts and lspci for GPU detection.
</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/02-host-management/02-CONTEXT.md
@.planning/phases/02-host-management/02-RESEARCH.md

<interfaces>
<!-- Expected hardware dict schema per D-13 -->
```python
# Hardware dict structure
{
    "architecture": str,       # e.g., "x86_64", "aarch64"
    "processor_cores": int,    # Physical cores
    "processor_count": int,    # Physical processors
    "memtotal_mb": int,        # Total memory in MB
    "mounts": [
        {
            "mount": str,           # Mount point, e.g., "/"
            "size_total": int,      # Total bytes
            "size_available": int   # Available bytes
        }
    ],
    "gpu": {
        "present": bool,
        "vendor": str | None   # "nvidia", "amd", "intel", or None
    }
}
```

<!-- Ansible facts available per RESEARCH.md -->
```python
# ansible_runner facts dict keys
facts = {
    'ansible_architecture': 'x86_64',
    'ansible_processor_cores': 4,
    'ansible_processor_count': 1,
    'ansible_memtotal_mb': 16384,
    'ansible_mounts': [
        {
            'mount': '/',
            'size_total': 500000000000,
            'size_available': 200000000000,
            'fstype': 'ext4',
            # ... other fields
        }
    ],
    # Note: GPU NOT in standard facts - requires lspci command
}
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Implement Ansible facts extraction</name>
  <files>src/clawrium/core/hardware.py, tests/test_hardware.py</files>
  <read_first>
    - tests/test_hardware.py (test stubs to make pass)
    - tests/conftest.py (mock_ansible_runner fixture)
    - .planning/phases/02-host-management/02-RESEARCH.md (Pattern 4 for ansible-runner usage)
  </read_first>
  <behavior>
    - extract_hardware_from_facts(facts) extracts architecture from facts['ansible_architecture']
    - extract_hardware_from_facts(facts) extracts processor_cores from facts['ansible_processor_cores']
    - extract_hardware_from_facts(facts) extracts processor_count from facts['ansible_processor_count']
    - extract_hardware_from_facts(facts) extracts memtotal_mb from facts['ansible_memtotal_mb']
    - extract_hardware_from_facts(facts) extracts mounts with mount, size_total, size_available fields
    - parse_gpu_output("NVIDIA") returns {"present": True, "vendor": "nvidia"}
    - parse_gpu_output("AMD") returns {"present": True, "vendor": "amd"}
    - parse_gpu_output("Intel") returns {"present": True, "vendor": "intel"}
    - parse_gpu_output("") returns {"present": False, "vendor": None}
  </behavior>
  <action>
Create src/clawrium/core/hardware.py with:

1. extract_hardware_from_facts(facts: dict) -> dict:
   ```python
   def extract_hardware_from_facts(facts: dict) -> dict:
       """Extract hardware capabilities from Ansible facts.

       Args:
           facts: Ansible fact dictionary from setup module

       Returns:
           Hardware dict with architecture, cores, memory, mounts
       """
       hardware = {
           'architecture': facts.get('ansible_architecture', 'unknown'),
           'processor_cores': facts.get('ansible_processor_cores', 0),
           'processor_count': facts.get('ansible_processor_count', 0),
           'memtotal_mb': facts.get('ansible_memtotal_mb', 0),
           'mounts': []
       }

       # Extract mount information (only relevant fields)
       for mount in facts.get('ansible_mounts', []):
           hardware['mounts'].append({
               'mount': mount.get('mount', ''),
               'size_total': mount.get('size_total', 0),
               'size_available': mount.get('size_available', 0)
           })

       return hardware
   ```

2. parse_gpu_output(lspci_output: str) -> dict:
   ```python
   def parse_gpu_output(lspci_output: str) -> dict:
       """Parse lspci VGA output to detect GPU presence and vendor.

       Args:
           lspci_output: Output from 'lspci | grep -i vga' command

       Returns:
           Dict with 'present' bool and 'vendor' string or None
       """
       if not lspci_output or not lspci_output.strip():
           return {'present': False, 'vendor': None}

       output_lower = lspci_output.lower()

       if 'nvidia' in output_lower:
           return {'present': True, 'vendor': 'nvidia'}
       elif 'amd' in output_lower or 'ati' in output_lower:
           return {'present': True, 'vendor': 'amd'}
       elif 'intel' in output_lower:
           return {'present': True, 'vendor': 'intel'}
       else:
           # GPU present but unknown vendor
           return {'present': True, 'vendor': 'unknown'}
   ```

Then update tests/test_hardware.py:
- Remove xfail markers from test_parse_ansible_facts_basic, test_parse_ansible_facts_mounts
- Remove xfail markers from test_detect_gpu_nvidia, test_detect_gpu_amd, test_detect_gpu_intel, test_detect_gpu_none
- Keep xfail on test_gather_hardware_full (depends on Task 2)
  </action>
  <acceptance_criteria>
    - src/clawrium/core/hardware.py contains `def extract_hardware_from_facts`
    - src/clawrium/core/hardware.py contains `def parse_gpu_output`
    - src/clawrium/core/hardware.py contains `ansible_architecture`
    - src/clawrium/core/hardware.py contains `ansible_processor_cores`
    - src/clawrium/core/hardware.py contains `ansible_mounts`
    - src/clawrium/core/hardware.py contains `'nvidia'` and `'amd'` and `'intel'`
    - `pytest tests/test_hardware.py::test_parse_ansible_facts_basic -x` exits 0
    - `pytest tests/test_hardware.py::test_detect_gpu_nvidia -x` exits 0
  </acceptance_criteria>
  <verify>
    <automated>pytest tests/test_hardware.py -k "parse_ansible_facts or detect_gpu" -x -v</automated>
  </verify>
  <done>6 tests pass (2 facts extraction + 4 GPU parsing)</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Implement full hardware gathering</name>
  <files>src/clawrium/core/hardware.py, tests/test_hardware.py</files>
  <read_first>
    - src/clawrium/core/hardware.py (after Task 1)
    - tests/test_hardware.py (test_gather_hardware_full stub)
    - tests/conftest.py (mock_ansible_runner fixture)
    - .planning/phases/02-host-management/02-RESEARCH.md (Pattern 4 for ansible-runner)
  </read_first>
  <behavior>
    - gather_hardware(hostname, user, ssh_key) returns complete hardware dict
    - gather_hardware runs ansible-runner with setup module to get facts
    - gather_hardware runs ansible-runner with command module for lspci GPU detection
    - gather_hardware combines facts extraction and GPU parsing into single dict
    - gather_hardware raises RuntimeError if ansible-runner fails
  </behavior>
  <action>
Add to src/clawrium/core/hardware.py:

```python
import ansible_runner
import tempfile
import json
from pathlib import Path

def gather_hardware(
    hostname: str,
    user: str = "xclm",
    port: int = 22,
    ssh_key: str | None = None
) -> dict:
    """Gather hardware capabilities from remote host.

    Uses ansible-runner to:
    1. Run setup module for standard facts (CPU, memory, disk)
    2. Run lspci command for GPU detection

    Args:
        hostname: Remote host IP or hostname
        user: SSH username (default: xclm)
        port: SSH port (default: 22)
        ssh_key: Optional path to SSH private key

    Returns:
        Hardware dict with architecture, cores, memory, mounts, gpu

    Raises:
        RuntimeError: If fact gathering fails
    """
    with tempfile.TemporaryDirectory() as tmpdir:
        # Create inventory
        inventory = {
            'all': {
                'hosts': {
                    hostname: {
                        'ansible_user': user,
                        'ansible_port': port,
                    }
                }
            }
        }

        if ssh_key:
            inventory['all']['hosts'][hostname]['ansible_ssh_private_key_file'] = ssh_key

        inv_path = Path(tmpdir) / 'inventory'
        inv_path.mkdir()
        (inv_path / 'hosts.json').write_text(json.dumps(inventory))

        # Gather standard facts
        result = ansible_runner.run(
            private_data_dir=tmpdir,
            host_pattern=hostname,
            module='setup',
            quiet=True
        )

        if result.status != 'successful':
            raise RuntimeError(f"Fact gathering failed: {result.status}")

        facts = result.get_fact_cache(hostname)
        if not facts:
            raise RuntimeError("No facts returned from host")

        # Extract hardware from facts
        hardware = extract_hardware_from_facts(facts)

        # GPU detection via lspci
        gpu_result = ansible_runner.run(
            private_data_dir=tmpdir,
            host_pattern=hostname,
            module='command',
            module_args='lspci | grep -i vga',
            quiet=True
        )

        # Parse GPU output from events
        gpu_output = ""
        if gpu_result.status == 'successful':
            for event in gpu_result.events:
                if event.get('event') == 'runner_on_ok':
                    gpu_output = event.get('event_data', {}).get('res', {}).get('stdout', '')
                    break

        hardware['gpu'] = parse_gpu_output(gpu_output)

        return hardware
```

Then update tests/test_hardware.py:
- Remove xfail marker from test_gather_hardware_full
- Ensure test properly mocks ansible_runner.run() and events
  </action>
  <acceptance_criteria>
    - src/clawrium/core/hardware.py contains `def gather_hardware`
    - src/clawrium/core/hardware.py contains `import ansible_runner`
    - src/clawrium/core/hardware.py contains `ansible_runner.run`
    - src/clawrium/core/hardware.py contains `module='setup'`
    - src/clawrium/core/hardware.py contains `lspci | grep -i vga`
    - src/clawrium/core/hardware.py contains `get_fact_cache`
    - tests/test_hardware.py does NOT contain `xfail` (all markers removed)
    - `pytest tests/test_hardware.py -x` exits 0
  </acceptance_criteria>
  <verify>
    <automated>pytest tests/test_hardware.py -x -v</automated>
  </verify>
  <done>All 7 hardware tests pass, hardware.py implements full hardware gathering</done>
</task>

</tasks>

<verification>
After all tasks complete:

```bash
# Run all hardware tests
pytest tests/test_hardware.py -v

# Verify module imports work
python -c "from clawrium.core.hardware import gather_hardware, extract_hardware_from_facts, parse_gpu_output"

# Check ansible-runner is available
python -c "import ansible_runner; print(ansible_runner.__version__)"
```
</verification>

<success_criteria>
- hardware.py provides gather_hardware, extract_hardware_from_facts, parse_gpu_output
- All tests in test_hardware.py pass (7 tests)
- Hardware dict includes architecture, processor_cores, processor_count, memtotal_mb, mounts, gpu
- GPU detection works for nvidia, amd, intel vendors
- ansible-runner integration works with setup module and command module
</success_criteria>

<output>
After completion, create `.planning/phases/02-host-management/02-03-SUMMARY.md`
</output>
