# Subagent & Agent Team Visibility for AMC

> **Version**: 1.0
> **Status**: Draft
> **Last Updated**: 2026-02-26

## Summary

Add comprehensive visibility into Claude Code subagents and agent teams within AMC. This enables users to monitor, understand, and debug multi-agent workflows by surfacing subagent activity, team coordination, and task progress directly in the dashboard.

---

## Problem Statement

Claude Code supports two multi-agent patterns that are currently invisible to AMC:

1. **Subagents** (via Task tool): Lightweight, isolated workers that execute in parallel and report results back to a parent session. Their conversations are stored in separate JSONL files but never surfaced.

2. **Agent Teams**: Full independent sessions with bidirectional messaging, shared task lists, and coordinated work. Team state lives in separate directories but is never parsed.

Without visibility into these, users cannot:
- See what subagents are doing in real-time
- Understand which tasks are being worked on by which agents
- Debug multi-agent coordination failures
- Monitor team member status and progress
- Trace the parent-child relationships in complex workflows

---

## User Workflows

### Workflow 1: Monitor Active Subagents

**Actor**: User with a Claude Code session that spawned subagents

**Flow**:
1. User views session card in AMC dashboard
2. Dashboard shows "3 subagents active" indicator
3. User clicks to expand subagent panel
4. Each subagent shows: type, status (running/complete), duration, last activity
5. User clicks a subagent to view its conversation in a modal
6. Real-time updates show subagent progress without refresh

### Workflow 2: Team Dashboard

**Actor**: User running an agent team

**Flow**:
1. AMC discovers active team from `~/.claude/teams/`
2. Dashboard shows team card with: team name, description, member count
3. User expands to see all team members: name, type, model, status
4. Task list panel shows: tasks by status (pending/in_progress/completed), assignments, dependencies
5. User can view any teammate's conversation
6. Real-time updates show task transitions and team messaging

### Workflow 3: Trace Parent-Child Relationships

**Actor**: User debugging a multi-agent workflow

**Flow**:
1. User views a subagent's conversation
2. UI shows "Spawned by: [parent session]" with link
3. User can navigate between parent and child conversations
4. Parent conversation shows inline markers: "[Spawned: code-reviewer → completed 2m ago]"
5. User understands the full workflow lineage

---

## Acceptance Criteria

### Subagent Discovery

- **AC-1**: AMC discovers subagent JSONL files in `{session_dir}/subagents/agent-*.jsonl`
- **AC-2**: Subagent metadata (agentId, type, status) is extracted from JSONL headers
- **AC-3**: Subagents are associated with their parent session via shared `sessionId`
- **AC-4**: Subagent discovery runs during state collection (existing poll interval)

### Subagent Display

- **AC-5**: Session cards show subagent count badge when subagents exist
- **AC-6**: Expandable panel lists all subagents with: type, status, duration, last activity
- **AC-7**: Clicking a subagent opens its conversation in a modal
- **AC-8**: Subagent conversation parsing reuses existing `_parse_claude_conversation` logic

### Subagent Status

- **AC-9**: Running subagents show animated activity indicator
- **AC-10**: Completed subagents show completion status with final message summary
- **AC-11**: Subagent status is derived from: file mtime recency, presence of stop markers

### Team Discovery

- **AC-12**: AMC discovers teams from `~/.claude/teams/*/config.json`
- **AC-13**: Team config is parsed: name, description, leadAgentId, members[]
- **AC-14**: Team tasks are discovered from `~/.claude/tasks/{team_name}/`
- **AC-15**: Team member sessions are linked to their corresponding session files

### Team Display

- **AC-16**: Teams appear as distinct cards in the dashboard (separate from individual sessions)
- **AC-17**: Team card shows: name, description, member count, active task count
- **AC-18**: Expanded team view shows member list with: name, type, model, status
- **AC-19**: Task panel shows tasks grouped by status with assignment indicators

### Parent-Child Linking

- **AC-20**: Subagent conversations show "Spawned by: [parent]" header with navigation link
- **AC-21**: Parent conversations show inline markers for spawned subagents with status
- **AC-22**: Users can navigate bidirectionally between parent and child conversations

### Real-Time Updates

- **AC-23**: Subagent status updates in real-time via existing SSE/polling mechanism
- **AC-24**: Team state changes (task status, member joins) propagate without refresh
- **AC-25**: File mtime tracking extends to subagent and team state files

---

## Architecture

### Data Model Extensions

```python
# Existing Session model gains new fields:
{
  "session_id": "uuid",
  "agent": "claude",
  # ... existing fields ...

  # NEW: Subagent information
  "subagents": [
    {
      "agent_id": "a510a6b",
      "subagent_type": "Explore",  # from Task tool input
      "status": "running|completed",
      "started_at": "iso-timestamp",
      "completed_at": "iso-timestamp|null",
      "last_activity_at": "iso-timestamp",
      "message_count": 42,
      "transcript_path": "/path/to/agent-xxx.jsonl"
    }
  ],

  # NEW: Team membership (if this session is a team member)
  "team": {
    "name": "mission-control",
    "role": "lead|member",
    "member_name": "team-lead"  # from team config
  }
}

# NEW: Team model (separate from sessions)
{
  "type": "team",
  "name": "mission-control",
  "description": "Building Mission Control...",
  "lead_agent_id": "team-lead@mission-control",
  "lead_session_id": "uuid",
  "members": [
    {
      "agent_id": "team-lead@mission-control",
      "name": "team-lead",
      "agent_type": "team-lead",
      "model": "claude-opus-4-5-20251101",
      "status": "active|idle|offline",
      "session_id": "uuid|null"  # linked to session if found
    }
  ],
  "tasks": {
    "pending": 5,
    "in_progress": 2,
    "completed": 12,
    "items": [...]  # optional: full task list
  },
  "created_at": "iso-timestamp",
  "config_mtime_ns": 123456789
}
```

### File System Locations

| Data Type | Path Pattern |
|-----------|--------------|
| Parent session | `~/.claude/projects/{project}/{session_id}.jsonl` |
| Subagent transcript | `~/.claude/projects/{project}/{session_id}/subagents/agent-{agentId}.jsonl` |
| Team config | `~/.claude/teams/{team_name}/config.json` |
| Team inboxes | `~/.claude/teams/{team_name}/inboxes/` |
| Team tasks | `~/.claude/tasks/{team_name}/` |

### Parsing Logic

#### Subagent Discovery (in StateMixin)

```python
def _discover_subagents(self, session_id, project_dir):
    """Discover subagent JSONL files for a session."""
    subagents = []
    session_dir = self._get_session_dir(session_id, project_dir)
    subagents_dir = session_dir / "subagents"

    if not subagents_dir.exists():
        return subagents

    for jsonl_file in subagents_dir.glob("agent-*.jsonl"):
        # Extract agent_id from filename: agent-{agentId}.jsonl
        agent_id = jsonl_file.stem.replace("agent-", "")

        # Parse first entry for metadata
        metadata = self._parse_subagent_metadata(jsonl_file)

        subagents.append({
            "agent_id": agent_id,
            "subagent_type": metadata.get("subagent_type", "unknown"),
            "status": self._determine_subagent_status(jsonl_file),
            "started_at": metadata.get("started_at"),
            "last_activity_at": self._get_file_mtime_iso(jsonl_file),
            "message_count": metadata.get("message_count", 0),
            "transcript_path": str(jsonl_file),
        })

    return subagents
```

#### Subagent Type Extraction

The subagent type is found in the parent session's Task tool invocation:

```python
def _extract_subagent_type_from_parent(self, parent_jsonl, agent_id):
    """Extract subagent_type from parent's Task tool call that spawned this agent."""
    for line in parent_jsonl.read_text().splitlines():
        entry = json.loads(line)

        # Look for progress entries with this agent's ID
        if entry.get("type") == "progress":
            data = entry.get("data", {})
            if data.get("agentId") == agent_id:
                # The parentToolUseID links to the Task tool invocation
                parent_tool_use_id = entry.get("parentToolUseID")
                # Now find the Task tool_use with that ID to get subagent_type
                ...
```

#### Team Discovery (new mixin: TeamMixin)

```python
class TeamMixin:
    TEAMS_DIR = Path.home() / ".claude" / "teams"
    TASKS_DIR = Path.home() / ".claude" / "tasks"

    def _discover_teams(self):
        """Discover all active teams."""
        teams = []

        if not self.TEAMS_DIR.exists():
            return teams

        for team_dir in self.TEAMS_DIR.iterdir():
            if not team_dir.is_dir():
                continue

            config_file = team_dir / "config.json"
            if not config_file.exists():
                continue

            try:
                config = json.loads(config_file.read_text())
                team = self._build_team_model(config, team_dir.name)
                teams.append(team)
            except (json.JSONDecodeError, OSError):
                continue

        return teams

    def _build_team_model(self, config, team_name):
        """Build team model from config.json."""
        members = []
        for m in config.get("members", []):
            members.append({
                "agent_id": m.get("agentId"),
                "name": m.get("name"),
                "agent_type": m.get("agentType"),
                "model": m.get("model"),
                "status": self._determine_member_status(m),
                "session_id": self._find_member_session(m),
            })

        return {
            "type": "team",
            "name": config.get("name", team_name),
            "description": config.get("description", ""),
            "lead_agent_id": config.get("leadAgentId"),
            "lead_session_id": config.get("leadSessionId"),
            "members": members,
            "tasks": self._load_team_tasks(team_name),
            "created_at": self._timestamp_to_iso(config.get("createdAt")),
        }
```

### API Extensions

```
GET /api/state
  Response adds:
  {
    "sessions": [...],  // existing, now includes subagents[]
    "teams": [...],     // NEW: active teams
    "server_time": "..."
  }

GET /api/conversation/{session_id}
  Existing endpoint works for subagents too (by agent_id)

GET /api/subagent/{session_id}/{agent_id}
  NEW: Get subagent conversation specifically

GET /api/team/{team_name}
  NEW: Get detailed team info including tasks

GET /api/team/{team_name}/tasks
  NEW: Get full task list for a team
```

### Dashboard Components

#### SubagentPanel.js

```javascript
function SubagentPanel({ subagents, onSelectSubagent }) {
  if (!subagents?.length) return null;

  return (
    <div className="subagent-panel">
      <div className="subagent-header">
        <span className="subagent-count">{subagents.length} subagents</span>
      </div>
      <div className="subagent-list">
        {subagents.map(sa => (
          <SubagentRow
            key={sa.agent_id}
            subagent={sa}
            onClick={() => onSelectSubagent(sa)}
          />
        ))}
      </div>
    </div>
  );
}

function SubagentRow({ subagent, onClick }) {
  const isRunning = subagent.status === 'running';

  return (
    <div className="subagent-row" onClick={onClick}>
      <span className={`subagent-status ${subagent.status}`}>
        {isRunning && <ActivityIndicator />}
      </span>
      <span className="subagent-type">{subagent.subagent_type}</span>
      <span className="subagent-duration">
        {formatDuration(subagent.started_at, subagent.completed_at)}
      </span>
    </div>
  );
}
```

#### TeamCard.js

```javascript
function TeamCard({ team, onExpand }) {
  const activeCount = team.members.filter(m => m.status === 'active').length;
  const taskCounts = team.tasks;

  return (
    <div className="team-card">
      <div className="team-header">
        <span className="team-name">{team.name}</span>
        <span className="team-badge">Team</span>
      </div>
      <div className="team-description">{team.description}</div>
      <div className="team-stats">
        <span>{activeCount}/{team.members.length} members active</span>
        <span>{taskCounts.in_progress} tasks in progress</span>
      </div>
      <button onClick={onExpand}>View Team</button>
    </div>
  );
}
```

---

## Implementation Specs

### IMP-1: Subagent Discovery Mixin

**Fulfills**: AC-1, AC-2, AC-3, AC-4

**File**: `amc_server/mixins/subagent.py` (new)

```python
"""Subagent discovery and parsing mixin for AMC server."""

import json
import os
import time
from pathlib import Path


class SubagentMixin:
    """Discovers and parses Claude Code subagent sessions."""

    def _discover_subagents_for_session(self, session_id, project_dir):
        """Discover all subagent JSONL files for a given parent session.

        Subagents are stored in: {session_dir}/subagents/agent-{agentId}.jsonl
        They share the parent's sessionId but have their own agentId.

        Args:
            session_id: Parent session UUID
            project_dir: Parent session's project directory

        Returns:
            List of subagent dicts with metadata
        """
        subagents = []

        # Get session directory path
        session_dir = self._get_claude_session_dir(session_id, project_dir)
        if not session_dir:
            return subagents

        subagents_dir = session_dir / "subagents"
        if not subagents_dir.exists():
            return subagents

        for jsonl_file in subagents_dir.glob("agent-*.jsonl"):
            try:
                agent_id = self._extract_agent_id_from_filename(jsonl_file.name)
                metadata = self._parse_subagent_metadata(jsonl_file, session_id, project_dir)

                subagents.append({
                    "agent_id": agent_id,
                    "subagent_type": metadata.get("subagent_type", "unknown"),
                    "prompt_summary": metadata.get("prompt_summary", ""),
                    "status": self._determine_subagent_status(jsonl_file, metadata),
                    "started_at": metadata.get("started_at"),
                    "completed_at": metadata.get("completed_at"),
                    "last_activity_at": self._get_file_mtime_iso(jsonl_file),
                    "message_count": metadata.get("message_count", 0),
                    "transcript_path": str(jsonl_file),
                    "mtime_ns": self._get_file_mtime_ns(jsonl_file),
                })
            except Exception:
                continue

        # Sort by started_at (most recent first)
        subagents.sort(key=lambda s: s.get("started_at", ""), reverse=True)
        return subagents

    def _extract_agent_id_from_filename(self, filename):
        """Extract agentId from filename like 'agent-a510a6b.jsonl'."""
        # Handle both formats: agent-{id}.jsonl and agent-acompact-{id}.jsonl
        stem = filename.replace(".jsonl", "")
        if stem.startswith("agent-"):
            return stem[6:]  # Remove "agent-" prefix
        return stem

    def _parse_subagent_metadata(self, jsonl_file, parent_session_id, parent_project_dir):
        """Parse subagent JSONL to extract metadata.

        Reads first and last entries to determine:
        - Agent ID (from agentId field)
        - Subagent type (from parent's Task tool invocation)
        - Start/end timestamps
        - Message count
        """
        metadata = {
            "message_count": 0,
            "started_at": None,
            "completed_at": None,
            "subagent_type": "unknown",
            "prompt_summary": "",
        }

        first_entry = None
        last_entry = None
        message_count = 0

        try:
            with open(jsonl_file, 'r') as f:
                for line in f:
                    if not line.strip():
                        continue
                    try:
                        entry = json.loads(line)
                        if not isinstance(entry, dict):
                            continue

                        if first_entry is None:
                            first_entry = entry
                        last_entry = entry

                        # Count user and assistant messages
                        if entry.get("type") in ("user", "assistant"):
                            message_count += 1
                    except json.JSONDecodeError:
                        continue
        except OSError:
            return metadata

        metadata["message_count"] = message_count

        if first_entry:
            metadata["started_at"] = first_entry.get("timestamp")
            metadata["agent_id"] = first_entry.get("agentId")

        if last_entry:
            metadata["completed_at"] = last_entry.get("timestamp")

        # Extract subagent_type from parent session's Task tool invocation
        agent_id = metadata.get("agent_id")
        if agent_id:
            type_info = self._extract_subagent_type_from_parent(
                parent_session_id, parent_project_dir, agent_id
            )
            if type_info:
                metadata["subagent_type"] = type_info.get("subagent_type", "unknown")
                metadata["prompt_summary"] = type_info.get("prompt_summary", "")

        return metadata

    def _extract_subagent_type_from_parent(self, session_id, project_dir, agent_id):
        """Extract subagent_type and prompt from parent's Task tool invocation.

        The parent session contains progress entries with:
        - data.agentId matching our subagent
        - parentToolUseID linking to the Task tool_use

        We find the Task tool_use to extract subagent_type and prompt.
        """
        parent_file = self._get_claude_conversation_file(session_id, project_dir)
        if not parent_file or not parent_file.exists():
            return None

        # First pass: find the parentToolUseID for this agent
        parent_tool_use_id = None
        try:
            with open(parent_file, 'r') as f:
                for line in f:
                    if not line.strip():
                        continue
                    try:
                        entry = json.loads(line)
                        if entry.get("type") == "progress":
                            data = entry.get("data", {})
                            if data.get("agentId") == agent_id:
                                parent_tool_use_id = entry.get("parentToolUseID")
                                break
                    except json.JSONDecodeError:
                        continue
        except OSError:
            return None

        if not parent_tool_use_id:
            return None

        # Second pass: find the Task tool_use with that ID
        try:
            with open(parent_file, 'r') as f:
                for line in f:
                    if not line.strip():
                        continue
                    try:
                        entry = json.loads(line)
                        if entry.get("type") == "assistant":
                            message = entry.get("message", {})
                            content = message.get("content", [])
                            if not isinstance(content, list):
                                continue
                            for part in content:
                                if not isinstance(part, dict):
                                    continue
                                if part.get("type") == "tool_use" and part.get("name") == "Task":
                                    if part.get("id") == parent_tool_use_id:
                                        input_data = part.get("input", {})
                                        prompt = input_data.get("prompt", "")
                                        return {
                                            "subagent_type": input_data.get("subagent_type", "unknown"),
                                            "prompt_summary": prompt[:100] + "..." if len(prompt) > 100 else prompt,
                                        }
                    except json.JSONDecodeError:
                        continue
        except OSError:
            return None

        return None

    def _determine_subagent_status(self, jsonl_file, metadata):
        """Determine if subagent is running or completed.

        Heuristics:
        - If file modified within last 30 seconds: running
        - If has completion markers: completed
        - Otherwise: completed (assume finished)
        """
        try:
            mtime = jsonl_file.stat().st_mtime
            age = time.time() - mtime

            # Recently active = running
            if age < 30:
                return "running"

            # Check message count - very few messages might indicate still starting
            if metadata.get("message_count", 0) < 2 and age < 60:
                return "running"

            return "completed"
        except OSError:
            return "completed"

    def _get_file_mtime_iso(self, path):
        """Get file mtime as ISO timestamp."""
        try:
            from datetime import datetime, timezone
            mtime = path.stat().st_mtime
            return datetime.fromtimestamp(mtime, timezone.utc).isoformat()
        except OSError:
            return None

    def _get_file_mtime_ns(self, path):
        """Get file mtime in nanoseconds for change detection."""
        try:
            return path.stat().st_mtime_ns
        except OSError:
            return None

    def _get_claude_session_dir(self, session_id, project_dir):
        """Get the session directory path for a Claude Code session.

        Claude stores session data at:
        ~/.claude/projects/{encoded_project_dir}/{session_id}/
        """
        if not project_dir:
            return None

        claude_projects_dir = Path.home() / ".claude" / "projects"

        # Encode project_dir same way Claude does: replace / with -
        encoded = project_dir.replace("/", "-")
        if encoded.startswith("-"):
            encoded = encoded  # Keep leading dash

        session_dir = claude_projects_dir / encoded / session_id
        if session_dir.exists() and session_dir.is_dir():
            return session_dir

        return None
```

### IMP-2: Team Discovery Mixin

**Fulfills**: AC-12, AC-13, AC-14, AC-15

**File**: `amc_server/mixins/team.py` (new)

```python
"""Agent team discovery and parsing mixin for AMC server."""

import json
import time
from datetime import datetime, timezone
from pathlib import Path


class TeamMixin:
    """Discovers and parses Claude Code agent teams."""

    TEAMS_DIR = Path.home() / ".claude" / "teams"
    TASKS_DIR = Path.home() / ".claude" / "tasks"

    def _discover_teams(self):
        """Discover all active agent teams.

        Teams are stored at ~/.claude/teams/{team_name}/config.json

        Returns:
            List of team dicts with metadata
        """
        teams = []

        if not self.TEAMS_DIR.exists():
            return teams

        for team_dir in self.TEAMS_DIR.iterdir():
            if not team_dir.is_dir():
                continue

            config_file = team_dir / "config.json"
            if not config_file.exists():
                continue

            try:
                config = json.loads(config_file.read_text())
                team = self._build_team_model(config, team_dir.name)

                # Add mtime for change detection
                team["config_mtime_ns"] = config_file.stat().st_mtime_ns

                teams.append(team)
            except (json.JSONDecodeError, OSError):
                continue

        # Sort by creation time (most recent first)
        teams.sort(key=lambda t: t.get("created_at", ""), reverse=True)
        return teams

    def _build_team_model(self, config, team_name):
        """Build team model from config.json.

        Config structure:
        {
            "name": "team-name",
            "description": "...",
            "createdAt": 1772054633201,
            "leadAgentId": "team-lead@team-name",
            "leadSessionId": "uuid",
            "members": [
                {
                    "agentId": "team-lead@team-name",
                    "name": "team-lead",
                    "agentType": "team-lead",
                    "model": "claude-opus-4-5-20251101",
                    "joinedAt": 1772054633201,
                    "tmuxPaneId": "",
                    "cwd": "/path/to/project",
                    "subscriptions": []
                }
            ]
        }
        """
        members = []
        for m in config.get("members", []):
            member = {
                "agent_id": m.get("agentId"),
                "name": m.get("name"),
                "agent_type": m.get("agentType"),
                "model": m.get("model"),
                "joined_at": self._timestamp_to_iso(m.get("joinedAt")),
                "cwd": m.get("cwd"),
                "status": "unknown",  # Will be enriched later
                "session_id": None,    # Will be linked if found
            }
            members.append(member)

        # Load task summary
        tasks = self._load_team_tasks_summary(team_name)

        return {
            "type": "team",
            "name": config.get("name", team_name),
            "description": config.get("description", ""),
            "lead_agent_id": config.get("leadAgentId"),
            "lead_session_id": config.get("leadSessionId"),
            "members": members,
            "member_count": len(members),
            "tasks": tasks,
            "created_at": self._timestamp_to_iso(config.get("createdAt")),
        }

    def _load_team_tasks_summary(self, team_name):
        """Load task summary for a team.

        Tasks are stored at ~/.claude/tasks/{team_name}/
        """
        tasks_dir = self.TASKS_DIR / team_name

        summary = {
            "pending": 0,
            "in_progress": 0,
            "completed": 0,
            "total": 0,
        }

        if not tasks_dir.exists():
            return summary

        # Count tasks by parsing task files or .highwatermark
        for task_file in tasks_dir.glob("*.json"):
            try:
                task = json.loads(task_file.read_text())
                status = task.get("status", "pending")
                if status in summary:
                    summary[status] += 1
                summary["total"] += 1
            except (json.JSONDecodeError, OSError):
                continue

        return summary

    def _load_team_tasks_full(self, team_name):
        """Load full task list for a team (for detailed view)."""
        tasks_dir = self.TASKS_DIR / team_name
        tasks = []

        if not tasks_dir.exists():
            return tasks

        for task_file in tasks_dir.glob("*.json"):
            try:
                task = json.loads(task_file.read_text())
                tasks.append({
                    "id": task.get("id"),
                    "subject": task.get("subject"),
                    "description": task.get("description"),
                    "status": task.get("status", "pending"),
                    "owner": task.get("owner"),
                    "blocked_by": task.get("blockedBy", []),
                    "blocks": task.get("blocks", []),
                    "created_at": task.get("createdAt"),
                })
            except (json.JSONDecodeError, OSError):
                continue

        # Sort by ID (numeric order if possible)
        tasks.sort(key=lambda t: t.get("id", ""))
        return tasks

    def _timestamp_to_iso(self, ts):
        """Convert millisecond timestamp to ISO format."""
        if not ts:
            return None
        try:
            # Handle both milliseconds and seconds
            if ts > 1e12:
                ts = ts / 1000  # Convert from milliseconds
            return datetime.fromtimestamp(ts, timezone.utc).isoformat()
        except (ValueError, OSError):
            return None

    def _link_team_members_to_sessions(self, team, sessions):
        """Link team members to their corresponding session records.

        Updates member status and session_id based on active sessions.
        """
        # Build session lookup by session_id
        session_lookup = {s.get("session_id"): s for s in sessions}

        for member in team.get("members", []):
            # Check if lead session matches
            if member.get("agent_id") == team.get("lead_agent_id"):
                lead_session_id = team.get("lead_session_id")
                if lead_session_id in session_lookup:
                    session = session_lookup[lead_session_id]
                    member["session_id"] = lead_session_id
                    member["status"] = "active" if not session.get("is_dead") else "offline"
                else:
                    member["status"] = "offline"
            else:
                # Non-lead members: try to find matching session by cwd
                member["status"] = "unknown"
```

### IMP-3: State Mixin Integration

**Fulfills**: AC-4, AC-23, AC-24, AC-25

**File**: `amc_server/mixins/state.py` (modify)

Update `_build_state_payload` to include teams:

```python
def _build_state_payload(self):
    """Build `/api/state` payload data used by JSON and SSE endpoints."""
    sessions = self._collect_sessions()
    teams = self._discover_teams()

    # Link team members to sessions
    for team in teams:
        self._link_team_members_to_sessions(team, sessions)

    return {
        "sessions": sessions,
        "teams": teams,
        "server_time": datetime.now(timezone.utc).isoformat(),
    }
```

Update `_collect_sessions` to call subagent discovery:

```python
# In the session processing loop, after loading basic data:
if data.get("agent") == "claude":
    subagents = self._discover_subagents_for_session(
        data.get("session_id", ""),
        data.get("project_dir", "")
    )
    if subagents:
        data["subagents"] = subagents
```

### IMP-4: API Route Extensions

**Fulfills**: AC-7, AC-17

**File**: `amc_server/mixins/http.py` (modify)

Add new routes:

```python
# In _route_get():

elif path.startswith("/api/subagent/"):
    # /api/subagent/{session_id}/{agent_id}
    parts = path.split("/")
    if len(parts) >= 5:
        session_id = parts[3]
        agent_id = parts[4]
        self._serve_subagent_conversation(session_id, agent_id)
    else:
        self._send_error(400, "Invalid subagent path")

elif path.startswith("/api/team/"):
    # /api/team/{team_name} or /api/team/{team_name}/tasks
    parts = path.split("/")
    team_name = parts[3] if len(parts) > 3 else ""
    if len(parts) > 4 and parts[4] == "tasks":
        self._serve_team_tasks(team_name)
    else:
        self._serve_team_detail(team_name)
```

### IMP-5: Dashboard SubagentPanel Component

**Fulfills**: AC-5, AC-6, AC-7, AC-9, AC-10

**File**: `dashboard/components/SubagentPanel.js` (new)

```javascript
import { h } from 'preact';
import { useState } from 'preact/hooks';
import { AgentActivityIndicator } from './AgentActivityIndicator.js';
import { formatDuration, formatRelativeTime } from '../utils/formatting.js';

export function SubagentPanel({ subagents, onSelectSubagent }) {
  const [expanded, setExpanded] = useState(false);

  if (!subagents?.length) return null;

  const runningCount = subagents.filter(s => s.status === 'running').length;

  return (
    <div className="subagent-panel">
      <button
        className="subagent-toggle"
        onClick={() => setExpanded(!expanded)}
      >
        <span className="subagent-icon">
          {runningCount > 0 && <AgentActivityIndicator size="small" />}
        </span>
        <span className="subagent-count">
          {subagents.length} subagent{subagents.length !== 1 ? 's' : ''}
          {runningCount > 0 && ` (${runningCount} running)`}
        </span>
        <span className={`expand-arrow ${expanded ? 'expanded' : ''}`}>▶</span>
      </button>

      {expanded && (
        <div className="subagent-list">
          {subagents.map(sa => (
            <SubagentRow
              key={sa.agent_id}
              subagent={sa}
              onClick={() => onSelectSubagent(sa)}
            />
          ))}
        </div>
      )}
    </div>
  );
}

function SubagentRow({ subagent, onClick }) {
  const isRunning = subagent.status === 'running';
  const duration = formatDuration(subagent.started_at, subagent.completed_at);

  return (
    <div
      className={`subagent-row ${subagent.status}`}
      onClick={onClick}
      role="button"
      tabIndex={0}
    >
      <span className="subagent-status">
        {isRunning ? (
          <AgentActivityIndicator size="tiny" />
        ) : (
          <span className="status-dot completed" />
        )}
      </span>
      <span className="subagent-type">{subagent.subagent_type}</span>
      <span className="subagent-meta">
        {subagent.message_count} msgs • {duration}
      </span>
    </div>
  );
}
```

### IMP-6: Dashboard TeamCard Component

**Fulfills**: AC-16, AC-17, AC-18, AC-19

**File**: `dashboard/components/TeamCard.js` (new)

```javascript
import { h } from 'preact';
import { useState } from 'preact/hooks';
import { formatRelativeTime } from '../utils/formatting.js';

export function TeamCard({ team, sessions, onViewMember }) {
  const [expanded, setExpanded] = useState(false);

  const activeMembers = team.members.filter(m => m.status === 'active');
  const tasks = team.tasks;

  return (
    <div className="team-card">
      <div className="team-header">
        <span className="team-name">{team.name}</span>
        <span className="team-badge">Team</span>
      </div>

      {team.description && (
        <div className="team-description">{team.description}</div>
      )}

      <div className="team-stats">
        <span className="stat">
          <span className="stat-value">{activeMembers.length}</span>
          <span className="stat-label">/{team.member_count} active</span>
        </span>
        <span className="stat">
          <span className="stat-value">{tasks.in_progress}</span>
          <span className="stat-label">in progress</span>
        </span>
        <span className="stat">
          <span className="stat-value">{tasks.completed}</span>
          <span className="stat-label">completed</span>
        </span>
      </div>

      <button
        className="team-expand"
        onClick={() => setExpanded(!expanded)}
      >
        {expanded ? 'Collapse' : 'View Details'}
      </button>

      {expanded && (
        <div className="team-details">
          <TeamMemberList
            members={team.members}
            onViewMember={onViewMember}
          />
          <TeamTaskSummary tasks={tasks} />
        </div>
      )}
    </div>
  );
}

function TeamMemberList({ members, onViewMember }) {
  return (
    <div className="team-members">
      <h4>Members</h4>
      {members.map(m => (
        <div
          key={m.agent_id}
          className={`team-member ${m.status}`}
          onClick={() => m.session_id && onViewMember(m.session_id)}
        >
          <span className={`member-status ${m.status}`} />
          <span className="member-name">{m.name}</span>
          <span className="member-type">{m.agent_type}</span>
          <span className="member-model">{m.model?.split('-').slice(-2, -1)[0]}</span>
        </div>
      ))}
    </div>
  );
}

function TeamTaskSummary({ tasks }) {
  return (
    <div className="team-tasks">
      <h4>Tasks</h4>
      <div className="task-progress">
        <div
          className="progress-bar"
          style={{
            '--completed': tasks.completed,
            '--in-progress': tasks.in_progress,
            '--pending': tasks.pending,
            '--total': tasks.total || 1
          }}
        />
      </div>
      <div className="task-counts">
        <span className="pending">{tasks.pending} pending</span>
        <span className="in-progress">{tasks.in_progress} in progress</span>
        <span className="completed">{tasks.completed} done</span>
      </div>
    </div>
  );
}
```

### IMP-7: Styles for Subagent/Team UI

**Fulfills**: AC-5, AC-6, AC-9, AC-16, AC-17

**File**: `dashboard/styles.css` (additions)

```css
/* Subagent Panel */
.subagent-panel {
  margin-top: 8px;
  border-top: 1px solid var(--border-color, #333);
  padding-top: 8px;
}

.subagent-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  background: transparent;
  border: none;
  color: var(--text-secondary, #999);
  cursor: pointer;
  padding: 4px 0;
  width: 100%;
  text-align: left;
}

.subagent-toggle:hover {
  color: var(--text-primary, #fff);
}

.subagent-count {
  flex: 1;
  font-size: 12px;
}

.expand-arrow {
  font-size: 10px;
  transition: transform 0.2s;
}

.expand-arrow.expanded {
  transform: rotate(90deg);
}

.subagent-list {
  margin-top: 8px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.subagent-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  background: var(--surface-secondary, #222);
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
}

.subagent-row:hover {
  background: var(--surface-hover, #333);
}

.subagent-row.running {
  border-left: 2px solid var(--color-active, #4CAF50);
}

.subagent-row.completed {
  border-left: 2px solid var(--color-done, #666);
}

.subagent-status {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 16px;
}

.status-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--color-done, #666);
}

.subagent-type {
  font-weight: 500;
  color: var(--text-primary, #fff);
}

.subagent-meta {
  margin-left: auto;
  color: var(--text-secondary, #999);
}

/* Team Card */
.team-card {
  background: var(--surface-primary, #1a1a1a);
  border: 1px solid var(--border-color, #333);
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 12px;
}

.team-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.team-name {
  font-size: 16px;
  font-weight: 600;
  color: var(--text-primary, #fff);
}

.team-badge {
  font-size: 10px;
  padding: 2px 6px;
  background: var(--color-team, #9C27B0);
  color: #fff;
  border-radius: 4px;
  text-transform: uppercase;
  font-weight: 600;
}

.team-description {
  font-size: 13px;
  color: var(--text-secondary, #999);
  margin-bottom: 12px;
}

.team-stats {
  display: flex;
  gap: 16px;
  margin-bottom: 12px;
}

.team-stats .stat {
  display: flex;
  align-items: baseline;
  gap: 4px;
}

.stat-value {
  font-size: 18px;
  font-weight: 600;
  color: var(--text-primary, #fff);
}

.stat-label {
  font-size: 12px;
  color: var(--text-secondary, #999);
}

.team-expand {
  background: var(--surface-secondary, #222);
  border: 1px solid var(--border-color, #333);
  color: var(--text-primary, #fff);
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
  width: 100%;
}

.team-expand:hover {
  background: var(--surface-hover, #333);
}

.team-details {
  margin-top: 16px;
  padding-top: 16px;
  border-top: 1px solid var(--border-color, #333);
}

.team-members h4,
.team-tasks h4 {
  font-size: 12px;
  text-transform: uppercase;
  color: var(--text-secondary, #999);
  margin-bottom: 8px;
}

.team-member {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px;
  background: var(--surface-secondary, #222);
  border-radius: 4px;
  margin-bottom: 4px;
  cursor: pointer;
}

.team-member:hover {
  background: var(--surface-hover, #333);
}

.member-status {
  width: 8px;
  height: 8px;
  border-radius: 50%;
}

.member-status.active {
  background: var(--color-active, #4CAF50);
}

.member-status.idle {
  background: var(--color-warning, #FF9800);
}

.member-status.offline,
.member-status.unknown {
  background: var(--color-done, #666);
}

.member-name {
  font-weight: 500;
  color: var(--text-primary, #fff);
}

.member-type {
  color: var(--text-secondary, #999);
  font-size: 12px;
}

.member-model {
  margin-left: auto;
  font-size: 11px;
  color: var(--text-tertiary, #666);
}

.team-tasks {
  margin-top: 16px;
}

.task-progress {
  height: 8px;
  background: var(--surface-secondary, #222);
  border-radius: 4px;
  overflow: hidden;
  margin-bottom: 8px;
}

.progress-bar {
  height: 100%;
  display: flex;
}

.progress-bar::before {
  content: '';
  height: 100%;
  width: calc(var(--completed) / var(--total) * 100%);
  background: var(--color-active, #4CAF50);
}

.progress-bar::after {
  content: '';
  height: 100%;
  width: calc(var(--in-progress) / var(--total) * 100%);
  background: var(--color-warning, #FF9800);
}

.task-counts {
  display: flex;
  gap: 12px;
  font-size: 12px;
}

.task-counts .pending {
  color: var(--text-secondary, #999);
}

.task-counts .in-progress {
  color: var(--color-warning, #FF9800);
}

.task-counts .completed {
  color: var(--color-active, #4CAF50);
}
```

### IMP-8: Parent-Child Navigation

**Fulfills**: AC-20, AC-21, AC-22

**File**: `dashboard/components/ConversationModal.js` (modifications)

Add header showing parent/child relationship:

```javascript
function ConversationHeader({ session, subagent, onNavigateToParent }) {
  if (subagent) {
    return (
      <div className="conversation-header subagent">
        <span className="subagent-badge">{subagent.subagent_type}</span>
        <button
          className="navigate-parent"
          onClick={onNavigateToParent}
        >
          ← Back to parent session
        </button>
      </div>
    );
  }

  return (
    <div className="conversation-header">
      <span className="session-id">{session.session_id.slice(0, 8)}</span>
      {session.subagents?.length > 0 && (
        <span className="subagent-indicator">
          {session.subagents.length} subagent{session.subagents.length !== 1 ? 's' : ''}
        </span>
      )}
    </div>
  );
}
```

### IMP-9: App.js State Integration

**Fulfills**: AC-23, AC-24

**File**: `dashboard/components/App.js` (modifications)

Update state management to include teams:

```javascript
// Add teams to state
const [teams, setTeams] = useState([]);

// Update fetchState to handle teams
const fetchState = useCallback(async () => {
  const response = await fetch('/api/state');
  const data = await response.json();
  setSessions(data.sessions || []);
  setTeams(data.teams || []);  // NEW
}, []);

// Add handler for subagent selection
const handleSelectSubagent = useCallback((session, subagent) => {
  setSelectedSubagent({ session, subagent });
  setModalOpen(true);
}, []);

// Render teams section
{teams.length > 0 && (
  <section className="teams-section">
    <h2>Agent Teams</h2>
    {teams.map(team => (
      <TeamCard
        key={team.name}
        team={team}
        sessions={sessions}
        onViewMember={handleViewMember}
      />
    ))}
  </section>
)}
```

---

## Rollout Slices

### Slice 1: Subagent Discovery (Backend)

**Goal**: Discover and expose subagent metadata through the API

**Tasks**:
1. Create `SubagentMixin` class in `amc_server/mixins/subagent.py`
2. Implement `_discover_subagents_for_session()`
3. Implement `_parse_subagent_metadata()`
4. Implement `_extract_subagent_type_from_parent()`
5. Integrate with `StateMixin._collect_sessions()`
6. Add unit tests for subagent parsing

**Verification**:
- `GET /api/state` returns `subagents[]` for sessions with subagents
- Subagent type is correctly extracted from parent
- Status detection (running/completed) works

### Slice 2: Subagent UI (Frontend)

**Goal**: Display subagents in the dashboard

**Tasks**:
1. Create `SubagentPanel.js` component
2. Add subagent styles to `styles.css`
3. Integrate panel into `SessionCard.js`
4. Add subagent conversation endpoint (`/api/subagent/{session_id}/{agent_id}`)
5. Add modal view for subagent conversations
6. Test with real sessions

**Verification**:
- Session cards show subagent badge
- Expanding shows subagent list
- Clicking subagent opens conversation modal
- Running subagents show activity indicator

### Slice 3: Team Discovery (Backend)

**Goal**: Discover and expose team metadata through the API

**Tasks**:
1. Create `TeamMixin` class in `amc_server/mixins/team.py`
2. Implement `_discover_teams()`
3. Implement `_build_team_model()`
4. Implement `_load_team_tasks_summary()`
5. Implement `_link_team_members_to_sessions()`
6. Integrate with `StateMixin._build_state_payload()`
7. Add unit tests

**Verification**:
- `GET /api/state` returns `teams[]` when teams exist
- Team members are linked to sessions
- Task counts are accurate

### Slice 4: Team UI (Frontend)

**Goal**: Display teams in the dashboard

**Tasks**:
1. Create `TeamCard.js` component
2. Create `TeamMemberList` and `TeamTaskSummary` subcomponents
3. Add team styles to `styles.css`
4. Integrate into `App.js`
5. Add team detail endpoint (`/api/team/{team_name}`)
6. Add navigation from team member to session

**Verification**:
- Teams appear in dashboard
- Member list shows status
- Task progress bar renders correctly
- Can navigate to member sessions

### Slice 5: Polish & Integration

**Goal**: Complete integration and edge case handling

**Tasks**:
1. Add parent-child navigation in conversation modals
2. Handle stale/cleaned-up subagent files gracefully
3. Add loading states for async data
4. Test with real multi-agent workflows
5. Performance testing with many subagents
6. Documentation update

**Verification**:
- End-to-end workflow works
- No errors with missing/stale files
- Performance acceptable with 10+ subagents
- README updated

---

## Testing Strategy

### Unit Tests

1. **Subagent parsing**:
   - Extract agent_id from various filename formats
   - Parse metadata from sample JSONL
   - Extract subagent_type from parent Task tool calls
   - Status detection logic

2. **Team parsing**:
   - Parse sample config.json
   - Load task summaries
   - Timestamp conversion
   - Member-session linking

### Integration Tests

1. **API endpoints**:
   - `/api/state` includes subagents and teams
   - `/api/subagent/{session_id}/{agent_id}` returns conversation
   - `/api/team/{team_name}` returns team details

### Manual Testing

1. Run Claude Code with Task tool to create subagents
2. Verify subagents appear in dashboard
3. Create agent team with TeamCreate
4. Verify team appears in dashboard
5. Test navigation between parent and child conversations

---

## Open Questions

1. **Performance**: With many subagents (10+), should we lazy-load conversations or batch?

2. **Staleness**: How long should completed subagent data persist? Follow session cleanup rules?

3. **Team member sessions**: How to reliably link team members to their sessions when multiple sessions exist?

4. **Real-time updates**: Should we add file watchers for subagent/team directories, or rely on polling?

---

## References

- Claude Code Task tool documentation
- Claude Code agent teams documentation
- Existing AMC codebase (mixins, dashboard)
- Sample JSONL files from actual sessions
