---
phase: 04-installation-fleet-status
plan: 04
type: execute
wave: 3
depends_on: [04-02, 04-03]
files_modified:
  - src/clawrium/cli/status.py
  - src/clawrium/cli/main.py
  - tests/test_cli_status.py
autonomous: true
requirements: [STAT-01]

must_haves:
  truths:
    - "User can run clm status and see all claws across all hosts"
    - "Status display is claw-centric, grouped by claw type"
    - "Status shows live health check results (running/stopped/unknown)"
    - "Status includes claw name, version, host, and status"
  artifacts:
    - path: "src/clawrium/cli/status.py"
      provides: "Fleet status command with claw-centric display"
      exports: ["status"]
    - path: "src/clawrium/cli/main.py"
      provides: "Main CLI with status command registered"
      contains: "from clawrium.cli.status"
    - path: "tests/test_cli_status.py"
      provides: "CLI status command tests"
      min_lines: 60
  key_links:
    - from: "src/clawrium/cli/status.py"
      to: "src/clawrium/core/health.py"
      via: "check_claw_health import"
      pattern: "from clawrium.core.health import"
    - from: "src/clawrium/cli/status.py"
      to: "src/clawrium/core/hosts.py"
      via: "load_hosts for fleet enumeration"
      pattern: "from clawrium.core.hosts import load_hosts"
---

<objective>
Create the `clm status` CLI command that displays fleet-wide claw status with live health checks, grouped by claw type per D-12.

Purpose: Users can see at a glance what's running across their entire fleet.
Output: Working `clm status` command showing claw instances with live status.
</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/phases/04-installation-fleet-status/04-CONTEXT.md

<interfaces>
<!-- From Plan 03 - health.py -->
```python
class ClawStatus(str, Enum):
    RUNNING = "running"
    STOPPED = "stopped"
    UNKNOWN = "unknown"
    NOT_INSTALLED = "not_installed"

class HealthResult(TypedDict):
    claw: str
    host: str
    status: ClawStatus
    user: str | None
    error: str | None

def check_claw_health(claw_name: str, host: dict) -> HealthResult
def check_all_claws_on_host(host: dict) -> list[HealthResult]
```

<!-- From hosts.py - extended host schema -->
```python
# Host record with claws tracking:
{
    "hostname": "192.168.1.100",
    "claws": {
        "openclaw": {
            "version": "0.1.0",
            "status": "installed",
            "installed_at": "...",
            "user": "opc-192"
        }
    },
    ...
}

def load_hosts() -> list[dict]
```

<!-- From existing CLI patterns -->
From src/clawrium/cli/host.py:
```python
console = Console()
table = Table(title="...")
table.add_column("Name", style="cyan")
# Rich table output pattern
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create status CLI command with claw-centric fleet view</name>
  <files>src/clawrium/cli/status.py, src/clawrium/cli/main.py, tests/test_cli_status.py</files>
  <read_first>
    - src/clawrium/cli/host.py (Rich table patterns, console usage)
    - src/clawrium/cli/registry.py (simple command structure)
    - src/clawrium/cli/main.py (command registration pattern)
    - src/clawrium/core/health.py (check_claw_health, ClawStatus from Plan 03)
    - src/clawrium/core/hosts.py (load_hosts to enumerate fleet)
    - tests/test_cli_host.py (CLI testing patterns)
  </read_first>
  <behavior>
    - Test: clm status with no claws shows "No claws installed" message
    - Test: clm status shows table grouped by claw type (per D-12)
    - Test: clm status shows claw name, version, host, status columns (per D-14)
    - Test: clm status performs live health check (mocked)
    - Test: clm status colors: green for running, red for stopped, yellow for unknown
    - Test: clm status with --host filter shows only that host's claws
  </behavior>
  <action>
Create `src/clawrium/cli/status.py`:

```python
"""Fleet status command for viewing claw instances across hosts."""

from collections import defaultdict
from typing import Optional

import typer
from rich.console import Console
from rich.markup import escape
from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.table import Table

from clawrium.core.hosts import load_hosts, HostsFileCorruptedError
from clawrium.core.health import check_claw_health, ClawStatus

__all__ = ["status"]

console = Console()


def status(
    host: Optional[str] = typer.Option(
        None, "--host", "-H", help="Filter to specific host (hostname or alias)"
    ),
) -> None:
    """Show fleet status across all hosts.

    Displays claw instances grouped by claw type (per D-12) with live
    health check (per D-13). Shows name, version, host, status (per D-14).
    """
    # Load all hosts
    try:
        hosts = load_hosts()
    except HostsFileCorruptedError as e:
        console.print(f"[red]Error:[/red] {e}")
        raise typer.Exit(code=1)

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

    # Filter to specific host if requested
    if host:
        hosts = [h for h in hosts if h.get("hostname") == host or h.get("alias") == host]
        if not hosts:
            console.print(f"[red]Error:[/red] Host '{escape(host)}' not found")
            raise typer.Exit(code=1)

    # Collect all claws across hosts, grouped by claw type
    # Structure: {claw_name: [(host_record, claw_record), ...]}
    claws_by_type: dict[str, list[tuple[dict, dict]]] = defaultdict(list)

    for h in hosts:
        for claw_name, claw_record in h.get("claws", {}).items():
            claws_by_type[claw_name].append((h, claw_record))

    if not claws_by_type:
        console.print("No claws installed on any host.")
        console.print("Run 'clm install' to install a claw.")
        return

    # Perform live health checks with progress spinner
    health_results: dict[tuple[str, str], ClawStatus] = {}  # (claw, hostname) -> status

    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        console=console,
        transient=True,
    ) as progress:
        task = progress.add_task("Checking fleet health...", total=None)

        for claw_name, instances in claws_by_type.items():
            for h, claw_record in instances:
                progress.update(task, description=f"Checking {claw_name} on {h.get('alias') or h['hostname']}...")
                result = check_claw_health(claw_name, h)
                health_results[(claw_name, h["hostname"])] = result["status"]

    console.print()  # Blank line after progress

    # Display claw-centric view (per D-12)
    for claw_name in sorted(claws_by_type.keys()):
        instances = claws_by_type[claw_name]

        table = Table(title=f"[bold cyan]{escape(claw_name)}[/bold cyan]")
        table.add_column("Host", style="white")
        table.add_column("Version", style="green")
        table.add_column("User", style="dim")
        table.add_column("Status")
        table.add_column("Installed", style="dim")

        for h, claw_record in instances:
            display_host = h.get("alias") or h["hostname"]
            version = claw_record.get("version", "?")
            user = claw_record.get("user", "-")
            installed_at = claw_record.get("installed_at", "-")
            if installed_at and installed_at != "-":
                # Format as date only for readability
                installed_at = installed_at.split("T")[0]

            # Get live status with color coding
            live_status = health_results.get((claw_name, h["hostname"]), ClawStatus.UNKNOWN)

            if live_status == ClawStatus.RUNNING:
                status_display = "[green]running[/green]"
            elif live_status == ClawStatus.STOPPED:
                status_display = "[red]stopped[/red]"
            elif live_status == ClawStatus.NOT_INSTALLED:
                status_display = "[yellow]not installed[/yellow]"
            else:
                status_display = "[yellow]unknown[/yellow]"

            # Also show install state if failed
            install_status = claw_record.get("status", "")
            if install_status == "failed":
                status_display = f"[red]install failed[/red]"
            elif install_status == "installing":
                status_display = "[yellow]installing...[/yellow]"

            table.add_row(
                escape(display_host),
                version,
                escape(user) if user else "-",
                status_display,
                installed_at,
            )

        console.print(table)
        console.print()  # Space between claw types
```

Update `src/clawrium/cli/main.py` to add status command:

```python
# Add at imports:
from clawrium.cli.status import status as status_command

# Add after install command:
@app.command()
def status(
    host: Optional[str] = typer.Option(None, "--host", "-H", help="Filter to specific host"),
) -> None:
    """Show fleet status across all hosts."""
    status_command(host=host)
```

Create `tests/test_cli_status.py`:

```python
"""Tests for fleet status CLI command."""

import pytest
from unittest.mock import patch, MagicMock
from typer.testing import CliRunner

from clawrium.cli.main import app
from clawrium.core.health import ClawStatus


runner = CliRunner()


@pytest.fixture
def mock_hosts_with_claws():
    """Hosts with installed claws."""
    return [
        {
            "hostname": "192.168.1.100",
            "alias": "server1",
            "port": 22,
            "user": "xclm",
            "key_id": "server1",
            "claws": {
                "openclaw": {
                    "version": "0.1.0",
                    "status": "installed",
                    "installed_at": "2026-03-21T10:00:00Z",
                    "user": "opc-server1",
                }
            },
        },
        {
            "hostname": "192.168.1.101",
            "alias": "server2",
            "port": 22,
            "user": "xclm",
            "key_id": "server2",
            "claws": {
                "openclaw": {
                    "version": "0.1.0",
                    "status": "installed",
                    "installed_at": "2026-03-21T11:00:00Z",
                    "user": "opc-server2",
                }
            },
        },
    ]


def test_status_no_hosts():
    """No hosts shows message to add hosts."""
    with patch("clawrium.cli.status.load_hosts", return_value=[]):
        result = runner.invoke(app, ["status"])

    assert result.exit_code == 0
    assert "No hosts registered" in result.output


def test_status_no_claws():
    """Hosts with no claws shows install message."""
    hosts = [{"hostname": "192.168.1.100", "claws": {}}]

    with patch("clawrium.cli.status.load_hosts", return_value=hosts):
        result = runner.invoke(app, ["status"])

    assert result.exit_code == 0
    assert "No claws installed" in result.output


def test_status_shows_claw_table(mock_hosts_with_claws):
    """Status shows table grouped by claw type."""
    mock_health = MagicMock(return_value={
        "claw": "openclaw",
        "host": "192.168.1.100",
        "status": ClawStatus.RUNNING,
        "user": "opc-server1",
        "error": None,
    })

    with patch("clawrium.cli.status.load_hosts", return_value=mock_hosts_with_claws):
        with patch("clawrium.cli.status.check_claw_health", mock_health):
            result = runner.invoke(app, ["status"])

    assert result.exit_code == 0
    assert "openclaw" in result.output
    assert "server1" in result.output
    assert "0.1.0" in result.output


def test_status_shows_running_status(mock_hosts_with_claws):
    """Running claw shows green status."""
    mock_health = MagicMock(return_value={
        "claw": "openclaw",
        "host": "192.168.1.100",
        "status": ClawStatus.RUNNING,
        "user": "opc-server1",
        "error": None,
    })

    with patch("clawrium.cli.status.load_hosts", return_value=mock_hosts_with_claws):
        with patch("clawrium.cli.status.check_claw_health", mock_health):
            result = runner.invoke(app, ["status"])

    assert "running" in result.output


def test_status_shows_stopped_status(mock_hosts_with_claws):
    """Stopped claw shows red status."""
    mock_health = MagicMock(return_value={
        "claw": "openclaw",
        "host": "192.168.1.100",
        "status": ClawStatus.STOPPED,
        "user": "opc-server1",
        "error": None,
    })

    with patch("clawrium.cli.status.load_hosts", return_value=mock_hosts_with_claws):
        with patch("clawrium.cli.status.check_claw_health", mock_health):
            result = runner.invoke(app, ["status"])

    assert "stopped" in result.output


def test_status_host_filter(mock_hosts_with_claws):
    """--host flag filters to specific host."""
    mock_health = MagicMock(return_value={
        "claw": "openclaw",
        "host": "192.168.1.100",
        "status": ClawStatus.RUNNING,
        "user": "opc-server1",
        "error": None,
    })

    with patch("clawrium.cli.status.load_hosts", return_value=mock_hosts_with_claws):
        with patch("clawrium.cli.status.check_claw_health", mock_health):
            result = runner.invoke(app, ["status", "--host", "server1"])

    assert result.exit_code == 0
    assert "server1" in result.output
    # Health check should only be called once (for server1)
    assert mock_health.call_count == 1


def test_status_host_filter_not_found(mock_hosts_with_claws):
    """--host with unknown host shows error."""
    with patch("clawrium.cli.status.load_hosts", return_value=mock_hosts_with_claws):
        result = runner.invoke(app, ["status", "--host", "unknown"])

    assert result.exit_code == 1
    assert "not found" in result.output


def test_status_shows_failed_install():
    """Failed installation shows install failed status."""
    hosts = [{
        "hostname": "192.168.1.100",
        "alias": "server1",
        "claws": {
            "openclaw": {
                "version": "0.1.0",
                "status": "failed",
                "error": "Playbook failed",
                "user": "opc-server1",
            }
        },
    }]

    # Health check returns unknown since not really installed
    mock_health = MagicMock(return_value={
        "claw": "openclaw",
        "host": "192.168.1.100",
        "status": ClawStatus.STOPPED,
        "user": "opc-server1",
        "error": None,
    })

    with patch("clawrium.cli.status.load_hosts", return_value=hosts):
        with patch("clawrium.cli.status.check_claw_health", mock_health):
            result = runner.invoke(app, ["status"])

    assert "install failed" in result.output
```
  </action>
  <verify>
    <automated>cd /home/devashish/workspace/ric03uec/clawrium && python -m pytest tests/test_cli_status.py -v --tb=short 2>&1 | tail -40</automated>
  </verify>
  <acceptance_criteria>
    - src/clawrium/cli/status.py exists
    - status.py contains "def status" function
    - status.py contains "from clawrium.core.health import check_claw_health"
    - status.py contains "from clawrium.core.hosts import load_hosts"
    - status.py contains "Table" from rich.table for display
    - status.py contains color coding: "[green]running", "[red]stopped", "[yellow]unknown"
    - status.py groups output by claw type (claws_by_type dict)
    - src/clawrium/cli/main.py contains "from clawrium.cli.status import"
    - main.py contains "@app.command" for status
    - tests/test_cli_status.py exists with at least 6 test functions
    - All tests in test_cli_status.py pass
    - clm status --help shows --host option
  </acceptance_criteria>
  <done>clm status command with claw-centric view and live health checks</done>
</task>

</tasks>

<verification>
- clm status --help shows available options
- clm status (no args) shows all claws across all hosts
- clm status --host myhost filters to that host
- Output is grouped by claw type (D-12)
- Shows claw name, version, host, status (D-14)
- Live health check performed (D-13)
- Color coding for running/stopped/unknown
- All tests pass
</verification>

<success_criteria>
- Users can view fleet status with `clm status` (STAT-01)
- Claw-centric grouping (D-12)
- Live health checks (D-13)
- Essential info displayed (D-14)
- Tests cover all display scenarios
</success_criteria>

<output>
After completion, create `.planning/phases/04-installation-fleet-status/04-04-SUMMARY.md`
</output>
