---
phase: 02-host-management
plan: 04
type: execute
wave: 2
depends_on:
  - "02-02"
  - "02-03"
files_modified:
  - src/clawrium/cli/host.py
  - src/clawrium/cli/main.py
autonomous: true
requirements:
  - HOST-01
  - HOST-02
  - HOST-03
  - HOST-04

must_haves:
  truths:
    - "User can add a host and connection is tested before saving"
    - "User can list all registered hosts in a table"
    - "User can remove a host with confirmation prompt"
    - "User can check host status (connection, last seen)"
    - "Hardware is detected and stored when adding a host"
  artifacts:
    - path: "src/clawrium/cli/host.py"
      provides: "Host CLI commands"
      exports: ["host_app"]
      contains: "def add"
      min_lines: 100
    - path: "src/clawrium/cli/main.py"
      provides: "CLI entry point with host subcommand"
      contains: "host_app"
  key_links:
    - from: "src/clawrium/cli/host.py"
      to: "src/clawrium/core/hosts.py"
      via: "host storage imports"
      pattern: "from clawrium.core.hosts import"
    - from: "src/clawrium/cli/host.py"
      to: "src/clawrium/core/ssh_connection.py"
      via: "SSH testing import"
      pattern: "from clawrium.core.ssh_connection import"
    - from: "src/clawrium/cli/host.py"
      to: "src/clawrium/core/hardware.py"
      via: "hardware detection import"
      pattern: "from clawrium.core.hardware import"
    - from: "src/clawrium/cli/main.py"
      to: "src/clawrium/cli/host.py"
      via: "subcommand registration"
      pattern: "app.add_typer.*host"
---

<objective>
Implement CLI commands for host management: add, list, remove, status.

Purpose: Users interact with hosts via `clm host add/list/remove/status` commands. This plan integrates all core modules (hosts.py, ssh_connection.py, hardware.py) into the user-facing CLI.

Output: host.py CLI module with four commands, registered in main.py as a subcommand group.
</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
@.planning/phases/02-host-management/02-02-SUMMARY.md (if exists)
@.planning/phases/02-host-management/02-03-SUMMARY.md (if exists)

<interfaces>
<!-- From src/clawrium/core/hosts.py (Plan 02) -->
```python
def load_hosts() -> list[dict]
def save_hosts(hosts: list[dict]) -> None
def add_host(host: dict) -> None
def remove_host(hostname: str) -> bool
def get_host(identifier: str) -> dict | None
```

<!-- From src/clawrium/core/ssh_connection.py (Plan 02) -->
```python
def get_ssh_config(hostname: str) -> dict
def test_ssh_connection(
    hostname: str,
    port: int = 22,
    user: str = "xclm",
    key_filename: str | None = None
) -> tuple[bool, str]
```

<!-- From src/clawrium/core/hardware.py (Plan 03) -->
```python
def gather_hardware(
    hostname: str,
    user: str = "xclm",
    port: int = 22,
    ssh_key: str | None = None
) -> dict
```

<!-- From src/clawrium/cli/init.py (existing pattern) -->
```python
from rich.console import Console
from rich.table import Table

console = Console()

# Table pattern:
table = Table(title="...")
table.add_column("...", style="...")
table.add_row(...)
console.print(table)
```

<!-- From src/clawrium/cli/main.py (existing structure) -->
```python
app = typer.Typer(
    name="clm",
    help="Clawrium - Manage your AI assistant fleet",
    no_args_is_help=True,
    add_completion=False,
)

# To add subcommand:
# host_app = typer.Typer(...)
# app.add_typer(host_app, name="host")
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Implement clm host add command</name>
  <files>src/clawrium/cli/host.py, tests/test_cli_host.py</files>
  <read_first>
    - tests/test_cli_host.py (test stubs for add command)
    - src/clawrium/cli/init.py (CLI patterns)
    - src/clawrium/core/hosts.py (host storage API)
    - src/clawrium/core/ssh_connection.py (connection testing API)
    - src/clawrium/core/hardware.py (hardware gathering API)
    - .planning/phases/02-host-management/02-CONTEXT.md (D-07 to D-12 for add flow)
  </read_first>
  <behavior>
    - clm host add <hostname> tests SSH connection before saving
    - clm host add <hostname> --user xclm --port 22 uses provided values
    - clm host add <hostname> --alias myhost stores alias with host
    - clm host add <hostname> detects hardware after successful connection
    - clm host add <hostname> with connection failure shows error and exits 1
    - clm host add <hostname> for duplicate hostname shows error and exits 1
  </behavior>
  <action>
Create src/clawrium/cli/host.py with:

```python
"""Host management commands for Clawrium."""

from datetime import datetime, timezone
from typing import Optional

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

from clawrium.core.hosts import add_host, get_host, load_hosts, remove_host
from clawrium.core.ssh_connection import get_ssh_config, test_ssh_connection
from clawrium.core.hardware import gather_hardware

__all__ = ["host_app"]

console = Console()

host_app = typer.Typer(
    name="host",
    help="Manage hosts in your fleet",
    no_args_is_help=True,
)


@host_app.command()
def add(
    hostname: str = typer.Argument(..., help="Host IP address or hostname"),
    port: Optional[int] = typer.Option(None, "--port", "-p", help="SSH port (default: 22)"),
    user: Optional[str] = typer.Option(None, "--user", "-u", help="SSH user (default: xclm)"),
    alias: Optional[str] = typer.Option(None, "--alias", "-a", help="Friendly name for this host"),
    key_path: Optional[str] = typer.Option(None, "--key", "-k", help="Path to SSH private key"),
    tags: Optional[str] = typer.Option(None, "--tags", "-t", help="Comma-separated tags"),
) -> None:
    """Add a new host to the fleet.

    Tests SSH connection before saving. Detects hardware capabilities
    automatically after successful connection.
    """
    # Check for duplicate
    existing = get_host(hostname)
    if existing:
        console.print(f"[red]Error:[/red] Host '{hostname}' already exists")
        raise typer.Exit(code=1)

    if alias:
        existing_alias = get_host(alias)
        if existing_alias:
            console.print(f"[red]Error:[/red] Alias '{alias}' already in use")
            raise typer.Exit(code=1)

    # Load SSH config and merge with provided values (per D-09)
    ssh_config = get_ssh_config(hostname)

    # CLI flags override SSH config (per D-07 hybrid input)
    final_port = port or int(ssh_config.get('port', 22))
    final_user = user or ssh_config.get('user', 'xclm')  # Default per D-11
    final_key = key_path or (ssh_config.get('identityfile', [None])[0] if 'identityfile' in ssh_config else None)

    # Prompt for missing values if interactive (per D-07)
    if final_port is None:
        final_port = typer.prompt("SSH port", default=22, type=int)
    if final_user is None:
        final_user = typer.prompt("SSH user", default="xclm")

    console.print(f"Testing connection to {hostname}:{final_port} as {final_user}...")

    # Test connection (per D-10)
    success, message = test_ssh_connection(
        hostname=hostname,
        port=final_port,
        user=final_user,
        key_filename=final_key
    )

    if not success:
        console.print(f"[red]Connection failed:[/red] {message}")
        raise typer.Exit(code=1)

    console.print(f"[green]Connection successful![/green]")

    # Detect hardware (per D-06)
    console.print("Detecting hardware capabilities...")
    try:
        hardware = gather_hardware(
            hostname=hostname,
            user=final_user,
            port=final_port,
            ssh_key=final_key
        )
        console.print(f"[green]Hardware detected:[/green] {hardware['architecture']}, "
                     f"{hardware['processor_cores']} cores, "
                     f"{hardware['memtotal_mb']}MB RAM")
    except Exception as e:
        console.print(f"[yellow]Warning:[/yellow] Could not detect hardware: {e}")
        hardware = {}

    # Build host record (per D-04)
    now = datetime.now(timezone.utc).isoformat()
    host = {
        "hostname": hostname,
        "port": final_port,
        "user": final_user,
        "auth_method": "key",
        "alias": alias,
        "key_path": final_key,
        "hardware": hardware,
        "metadata": {
            "added_at": now,
            "last_seen": now,
            "tags": [t.strip() for t in tags.split(",")] if tags else []
        }
    }

    add_host(host)
    console.print(f"[green]Host '{alias or hostname}' added successfully![/green]")
```

Then update tests/test_cli_host.py:
- Remove xfail markers from add-related tests
- Ensure tests mock ssh_connection and hardware modules
- Tests should use monkeypatch to mock the core module functions
  </action>
  <acceptance_criteria>
    - src/clawrium/cli/host.py contains `host_app = typer.Typer`
    - src/clawrium/cli/host.py contains `def add(`
    - src/clawrium/cli/host.py contains `from clawrium.core.hosts import`
    - src/clawrium/cli/host.py contains `from clawrium.core.ssh_connection import`
    - src/clawrium/cli/host.py contains `from clawrium.core.hardware import`
    - src/clawrium/cli/host.py contains `test_ssh_connection(`
    - src/clawrium/cli/host.py contains `gather_hardware(`
    - src/clawrium/cli/host.py contains `add_host(`
    - `pytest tests/test_cli_host.py -k "add" -x` exits 0
  </acceptance_criteria>
  <verify>
    <automated>pytest tests/test_cli_host.py -k "add" -x -v</automated>
  </verify>
  <done>4 add tests pass, clm host add command implemented with connection testing and hardware detection</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Implement clm host list command</name>
  <files>src/clawrium/cli/host.py, tests/test_cli_host.py</files>
  <read_first>
    - src/clawrium/cli/host.py (after Task 1)
    - tests/test_cli_host.py (test stubs for list command)
    - src/clawrium/cli/init.py (Rich table pattern)
    - .planning/phases/02-host-management/02-CONTEXT.md (D-17 for table output)
  </read_first>
  <behavior>
    - clm host list with no hosts shows "No hosts registered"
    - clm host list with hosts shows table with columns: Alias, Host, Architecture, Cores, Memory, Tags
    - Table uses Rich formatting consistent with init command
  </behavior>
  <action>
Add to src/clawrium/cli/host.py:

```python
@host_app.command()
def list() -> None:
    """List all registered hosts."""
    hosts = load_hosts()

    if not hosts:
        console.print("No hosts registered. Use 'clm host add' to add a host.")
        return

    table = Table(title="Registered Hosts")

    table.add_column("Alias", style="cyan")
    table.add_column("Host", style="white")
    table.add_column("Architecture", style="yellow")
    table.add_column("Cores", justify="right")
    table.add_column("Memory (GB)", justify="right")
    table.add_column("Tags", style="dim")

    for host in hosts:
        hw = host.get('hardware', {})
        meta = host.get('metadata', {})

        # Format memory as GB with 1 decimal
        mem_gb = round(hw.get('memtotal_mb', 0) / 1024, 1) if hw.get('memtotal_mb') else '-'

        table.add_row(
            host.get('alias') or '-',
            host['hostname'],
            hw.get('architecture', '?'),
            str(hw.get('processor_cores', '?')),
            str(mem_gb),
            ', '.join(meta.get('tags', [])) or '-'
        )

    console.print(table)
```

Update tests/test_cli_host.py:
- Remove xfail markers from list-related tests
- Ensure test_host_list_empty and test_host_list_table pass
  </action>
  <acceptance_criteria>
    - src/clawrium/cli/host.py contains `def list(`
    - src/clawrium/cli/host.py contains `Table(title="Registered Hosts")`
    - src/clawrium/cli/host.py contains `table.add_column("Alias"`
    - src/clawrium/cli/host.py contains `table.add_column("Architecture"`
    - src/clawrium/cli/host.py contains `No hosts registered`
    - `pytest tests/test_cli_host.py -k "list" -x` exits 0
  </acceptance_criteria>
  <verify>
    <automated>pytest tests/test_cli_host.py -k "list" -x -v</automated>
  </verify>
  <done>2 list tests pass, clm host list shows Rich table or empty message</done>
</task>

<task type="auto" tdd="true">
  <name>Task 3: Implement clm host remove and status commands</name>
  <files>src/clawrium/cli/host.py, src/clawrium/cli/main.py, tests/test_cli_host.py</files>
  <read_first>
    - src/clawrium/cli/host.py (after Task 2)
    - tests/test_cli_host.py (test stubs for remove and status)
    - src/clawrium/cli/main.py (for subcommand registration)
    - .planning/phases/02-host-management/02-CONTEXT.md (D-18, D-19 for remove/status)
  </read_first>
  <behavior>
    - clm host remove <hostname> prompts for confirmation
    - clm host remove <hostname> --force skips confirmation
    - clm host remove <nonexistent> shows error and exits 1
    - clm host status <hostname> shows connection status, last seen
    - clm host status <hostname> with unreachable host shows "Disconnected"
    - clm host status <hostname> --refresh re-detects hardware and updates record
  </behavior>
  <action>
Add to src/clawrium/cli/host.py:

```python
@host_app.command()
def remove(
    hostname: str = typer.Argument(..., help="Host hostname or alias to remove"),
    force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation prompt"),
) -> None:
    """Remove a host from the fleet.

    Prompts for confirmation unless --force is specified.
    """
    # Find host by hostname or alias
    host = get_host(hostname)
    if not host:
        console.print(f"[red]Error:[/red] Host '{hostname}' not found")
        raise typer.Exit(code=1)

    display_name = host.get('alias') or host['hostname']

    # Confirmation (per D-18)
    if not force:
        confirmed = typer.confirm(f"Remove host '{display_name}'? This cannot be undone.")
        if not confirmed:
            console.print("Cancelled.")
            raise typer.Abort()

    # Remove by actual hostname
    success = remove_host(host['hostname'])
    if success:
        console.print(f"[green]Host '{display_name}' removed successfully.[/green]")
    else:
        console.print(f"[red]Error:[/red] Failed to remove host")
        raise typer.Exit(code=1)


@host_app.command()
def status(
    hostname: str = typer.Argument(..., help="Host hostname or alias to check"),
    refresh: bool = typer.Option(False, "--refresh", "-r", help="Re-detect hardware capabilities"),
) -> None:
    """Check status of a host.

    Shows connection status, hostname verification, and last seen time.
    Use --refresh to update hardware information.
    """
    # Find host
    host = get_host(hostname)
    if not host:
        console.print(f"[red]Error:[/red] Host '{hostname}' not found")
        raise typer.Exit(code=1)

    display_name = host.get('alias') or host['hostname']
    console.print(f"Checking status of '{display_name}'...")

    # Test connection
    success, message = test_ssh_connection(
        hostname=host['hostname'],
        port=host.get('port', 22),
        user=host.get('user', 'xclm'),
        key_filename=host.get('key_path')
    )

    # Display status table
    table = Table(title=f"Host Status: {display_name}")
    table.add_column("Property", style="cyan")
    table.add_column("Value")

    if success:
        table.add_row("Connection", "[green]Connected[/green]")
    else:
        table.add_row("Connection", f"[red]Disconnected[/red] ({message})")

    table.add_row("Hostname", host['hostname'])
    table.add_row("Port", str(host.get('port', 22)))
    table.add_row("User", host.get('user', 'xclm'))

    meta = host.get('metadata', {})
    table.add_row("Added", meta.get('added_at', 'Unknown'))
    table.add_row("Last Seen", meta.get('last_seen', 'Unknown'))
    table.add_row("Tags", ', '.join(meta.get('tags', [])) or '-')

    hw = host.get('hardware', {})
    if hw:
        table.add_row("Architecture", hw.get('architecture', '?'))
        table.add_row("CPU Cores", str(hw.get('processor_cores', '?')))
        table.add_row("Memory", f"{round(hw.get('memtotal_mb', 0) / 1024, 1)} GB")
        gpu = hw.get('gpu', {})
        if gpu.get('present'):
            table.add_row("GPU", f"{gpu.get('vendor', 'unknown').upper()}")
        else:
            table.add_row("GPU", "None detected")

    console.print(table)

    # Refresh hardware if requested (per D-06)
    if refresh and success:
        console.print("\nRefreshing hardware information...")
        try:
            new_hardware = gather_hardware(
                hostname=host['hostname'],
                user=host.get('user', 'xclm'),
                port=host.get('port', 22),
                ssh_key=host.get('key_path')
            )

            # Update host record
            hosts = load_hosts()
            for h in hosts:
                if h['hostname'] == host['hostname']:
                    h['hardware'] = new_hardware
                    h['metadata']['last_seen'] = datetime.now(timezone.utc).isoformat()
                    break

            from clawrium.core.hosts import save_hosts
            save_hosts(hosts)

            console.print(f"[green]Hardware information updated.[/green]")
        except Exception as e:
            console.print(f"[yellow]Warning:[/yellow] Could not refresh hardware: {e}")
    elif refresh and not success:
        console.print("[yellow]Cannot refresh hardware: host is not connected[/yellow]")
```

Then update src/clawrium/cli/main.py to register host subcommand:

```python
"""Main CLI entry point for Clawrium."""

import typer

from clawrium.cli.init import init as init_command
from clawrium.cli.host import host_app

__all__ = ["app"]

app = typer.Typer(
    name="clm",
    help="Clawrium - Manage your AI assistant fleet",
    no_args_is_help=True,
    add_completion=False,
)


@app.callback(invoke_without_command=True)
def main(ctx: typer.Context) -> None:
    """Clawrium CLI main entry point."""
    if ctx.invoked_subcommand is None:
        pass


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


# Register host subcommands
app.add_typer(host_app, name="host")


if __name__ == "__main__":
    app()
```

Update tests/test_cli_host.py:
- Remove all remaining xfail markers
- Ensure all 12 tests pass
  </action>
  <acceptance_criteria>
    - src/clawrium/cli/host.py contains `def remove(`
    - src/clawrium/cli/host.py contains `def status(`
    - src/clawrium/cli/host.py contains `typer.confirm(`
    - src/clawrium/cli/host.py contains `--force`
    - src/clawrium/cli/host.py contains `--refresh`
    - src/clawrium/cli/main.py contains `from clawrium.cli.host import host_app`
    - src/clawrium/cli/main.py contains `app.add_typer(host_app, name="host")`
    - tests/test_cli_host.py does NOT contain `xfail` (all markers removed)
    - `pytest tests/test_cli_host.py -x` exits 0
  </acceptance_criteria>
  <verify>
    <automated>pytest tests/test_cli_host.py -x -v</automated>
  </verify>
  <done>All 12 CLI tests pass, host commands fully implemented and registered</done>
</task>

</tasks>

<verification>
After all tasks complete:

```bash
# Run all CLI host tests
pytest tests/test_cli_host.py -v

# Run all phase 2 tests
pytest tests/test_hosts.py tests/test_ssh_connection.py tests/test_hardware.py tests/test_cli_host.py -v

# Verify CLI commands are available
uv run clm --help
uv run clm host --help
uv run clm host add --help
uv run clm host list --help
uv run clm host remove --help
uv run clm host status --help
```
</verification>

<success_criteria>
- host.py provides add, list, remove, status commands
- Commands are accessible via `clm host <command>`
- All tests in test_cli_host.py pass (12 tests)
- All phase 2 tests pass (35+ tests total)
- `clm host add` tests connection before saving
- `clm host add` detects hardware automatically
- `clm host list` shows Rich table
- `clm host remove` prompts for confirmation
- `clm host status` shows connection status and hardware info
</success_criteria>

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