php-agentic/changelog/2026/jan/TASK-006-agent-plans-admin-ui.md
Snider ad83825f93 refactor: rename namespace Core\Agentic to Core\Mod\Agentic
Updates all classes to use the new modular namespace convention.
Adds Service/ layer with Core\Service\Agentic for service definition.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 16:12:58 +00:00

82 KiB

TASK-006: Agent Plans Admin UI

Status: all_phases_verified (Ready for human approval) Created: 2026-01-02 Last Updated: 2026-01-02 23:45 by Claude Opus 4.5 (Verification Agent) Assignee: Claude Opus 4.5 (Implementation Agent) Verifier: Claude Opus 4.5 (Verification Agent)


Critical Context (READ FIRST)

The agent infrastructure exists. Now we need visibility.

What Already Exists

The codebase has sophisticated agent workflow infrastructure:

Component Location Purpose
AgentPlan app/Models/Agent/AgentPlan.php Multi-phase work plans
AgentPhase app/Models/Agent/AgentPhase.php Individual phases with tasks
AgentSession app/Models/Agent/AgentSession.php Work sessions with handoff
AgentWorkspaceState app/Models/Agent/AgentWorkspaceState.php Shared state storage
McpToolCall app/Models/Mcp/McpToolCall.php Tool invocation logs
McpToolCallStat app/Models/Mcp/McpToolCallStat.php Daily aggregates
PlanTemplateService app/Services/Agent/PlanTemplateService.php Template-based plan creation
MCP Registry routes/mcp.php Agent discovery endpoints

What's Missing: Admin UI to view, manage, and monitor all of this.

The Vision

A Hades admin section that provides:

  1. Plan Dashboard — See all active plans across workspaces
  2. Phase Tracking — Visual progress through plan phases
  3. Session Monitor — Watch agent sessions in real-time
  4. Tool Analytics — MCP tool usage and performance
  5. API Key Management — Enable external agent access
  6. Plan Templates — Browse and create from templates

This becomes the control tower for AI-stabilised hosting — where humans supervise agent work.


Objective

Build a comprehensive Agent Plans admin UI in the Hades section that surfaces all existing agent infrastructure. Enable operators to monitor, manage, and guide agent work across the platform.

"Done" looks like:

  • Hades users can see all agent plans and their progress
  • Real-time session monitoring shows active agent work
  • MCP tool analytics reveal usage patterns
  • API keys allow external agents to create/update plans
  • Plan templates can be browsed and instantiated

Acceptance Criteria

Phase 1: Plan Dashboard

  • AC1: Route hub.agents.plans shows all AgentPlans (filterable by workspace, status)
  • AC2: Plan list shows: title, workspace, status, phase progress, last activity
  • AC3: Plan detail view shows all phases with task checklists
  • AC4: Phase progress displayed as visual percentage bar
  • AC5: Can manually update plan status (activate, complete, archive)
  • AC6: Can manually update phase status (start, complete, block, skip)

Phase 2: Session Monitor

  • AC7: Route hub.agents.sessions shows all AgentSessions
  • AC8: Active sessions show real-time activity (polling or WebSocket)
  • AC9: Session detail shows: work log, artifacts, handoff notes
  • AC10: Can view session context summary and final summary
  • AC11: Timeline view shows session sequence within a plan
  • AC12: Can pause/resume/fail active sessions manually

Phase 3: Tool Analytics

  • AC13: Route hub.agents.tools shows MCP tool usage dashboard
  • AC14: Top 10 tools by usage displayed
  • AC15: Daily trend chart (calls per day, 30-day window)
  • AC16: Server breakdown shows usage by MCP server
  • AC17: Error rate and average duration displayed per tool
  • AC18: Can drill down to individual tool calls with full params

Phase 4: API Key Management

  • AC19: Route hub.agents.api-keys manages agent API access
  • AC20: Can create API keys scoped to workspace
  • AC21: Keys have configurable permissions (read plans, write plans, execute tools)
  • AC22: Key usage tracking (last used, call count)
  • AC23: Can revoke keys immediately
  • AC24: Rate limiting configuration per key

Phase 5: Plan Templates

  • AC25: Route hub.agents.templates lists available plan templates
  • AC26: Template preview shows phases and variables
  • AC27: Can create new plan from template with variable input
  • AC28: Template categories displayed for organisation
  • AC29: Can import custom templates (YAML upload)

Implementation Checklist

Routes (routes/web.php)

Add after line 164 (after Commerce routes), inside the hub group:

// Agent Operations (Hades only)
Route::prefix('agents')->name('agents.')->group(function () {
    Route::get('/', \App\Livewire\Admin\Agent\Dashboard::class)->name('index');
    Route::get('/plans', \App\Livewire\Admin\Agent\Plans::class)->name('plans');
    Route::get('/plans/{slug}', \App\Livewire\Admin\Agent\PlanDetail::class)->name('plans.show');
    Route::get('/sessions', \App\Livewire\Admin\Agent\Sessions::class)->name('sessions');
    Route::get('/sessions/{id}', \App\Livewire\Admin\Agent\SessionDetail::class)->name('sessions.show');
    Route::get('/tools', \App\Livewire\Admin\Agent\ToolAnalytics::class)->name('tools');
    Route::get('/tools/calls', \App\Livewire\Admin\Agent\ToolCalls::class)->name('tools.calls');
    Route::get('/api-keys', \App\Livewire\Admin\Agent\ApiKeys::class)->name('api-keys');
    Route::get('/templates', \App\Livewire\Admin\Agent\Templates::class)->name('templates');
});
  • Add route group hub/agents
  • Route hub.agents.index → Dashboard (overview)
  • Route hub.agents.plans → Plans (list)
  • Route hub.agents.plans.show → PlanDetail (single plan by slug)
  • Route hub.agents.sessions → Sessions (list) (Phase 2)
  • Route hub.agents.sessions.show → SessionDetail (single session) (Phase 2)
  • Route hub.agents.tools → ToolAnalytics (MCP stats) (Phase 3)
  • Route hub.agents.tools.calls → ToolCalls (detailed logs) (Phase 3)
  • Route hub.agents.api-keys → ApiKeys (key management) (Phase 4)
  • Route hub.agents.templates → Templates (template browser) (Phase 5)

Livewire Components (app/Livewire/Admin/Agent/)

Create directory: mkdir -p app/Livewire/Admin/Agent

Each component should check $this->authorize() or auth()->user()->isHades() in mount().

  • Create Dashboard.php — Overview with key metrics

    • Query: AgentPlan::where('status', 'active')->count()
    • Query: AgentSession::where('status', 'active')->count()
    • Query: McpToolCallStat::getTopTools()
    • Query: McpToolCallStat::getDailyTrend()
  • Create Plans.php — Plan list with filters

    • Properties: $status, $workspace, $search, $perPage
    • Query: AgentPlan::with(['workspace', 'agentPhases'])->filter()->paginate()
    • Methods: activate(), archive(), delete()
  • Create PlanDetail.php — Single plan with phases

    • Property: AgentPlan $plan (route model binding via slug)
    • Relationships: $plan->agentPhases, $plan->sessions, $plan->states
    • Methods: updatePhaseStatus(), completeTask(), addTask()
  • Create Sessions.php — Session list

    • Properties: $status, $agentType, $planSlug, $workspace, $search
    • Query: AgentSession::with(['plan', 'workspace'])->filter()->paginate()
    • Methods: pause(), resume(), complete(), fail(), clearFilters()
  • Create SessionDetail.php — Single session with logs

    • Property: AgentSession $session
    • Display: $session->work_log, $session->artifacts, $session->handoff_notes
    • Methods: pauseSession(), resumeSession(), completeSession(), failSession(), poll()
    • Polling: 5-second interval for active sessions (disabled when ended)
  • Create ToolAnalytics.php — MCP usage charts

    • Query: McpToolCallStat::getTopTools(), getDailyTrend(), getServerStats()
    • Chart data for Chart.js with daily trend visualization
    • Filters: days (7/14/30), workspace, server
  • Create ToolCalls.php — Tool call log browser

    • Properties: $search, $server, $tool, $status, $workspace, $agentType
    • Query: McpToolCall::with(['workspace'])->filter()->paginate()
    • Modal detail view for individual tool calls (AC18)
  • Create ApiKeys.php — Key CRUD (Phase 4)

    • Methods: createKey(), updateKey(), revokeKey()
    • Display plaintext key ONCE on creation
    • Filters: workspace, status (active/revoked/expired)
    • Edit modal for permissions and rate limit updates
  • Create Templates.php — Template browser and instantiation (Phase 5)

    • Uses: PlanTemplateService::list(), get(), createPlan(), previewTemplate()
    • Modal for variable input before instantiation
    • YAML import with validation and preview
    • Category filtering and search
    • Template preview with phases and variables

Views (resources/views/admin/livewire/agent/)

  • Create dashboard.blade.php
  • Create plans.blade.php
  • Create plan-detail.blade.php
  • Create sessions.blade.php (Phase 2)
  • Create session-detail.blade.php (Phase 2)
  • Create tool-analytics.blade.php (Phase 3)
  • Create tool-calls.blade.php (Phase 3)
  • Create api-keys.blade.php (Phase 4)
  • Create templates.blade.php (Phase 5)

API Endpoints (routes/api.php)

  • Add API route group api/v1/agents with API key auth
  • GET /api/v1/agents/plans — List plans for workspace
  • POST /api/v1/agents/plans — Create plan (from template or raw)
  • GET /api/v1/agents/plans/{slug} — Get plan detail
  • PATCH /api/v1/agents/plans/{slug} — Update plan status
  • POST /api/v1/agents/plans/{slug}/phases/{id}/tasks — Add task to phase
  • PATCH /api/v1/agents/plans/{slug}/phases/{id}/tasks/{name} — Complete task
  • POST /api/v1/agents/sessions — Start new session
  • PATCH /api/v1/agents/sessions/{id} — Update session (log, artifacts, handoff)
  • POST /api/v1/agents/sessions/{id}/complete — End session with summary
  • GET /api/v1/agents/templates — List available templates
  • POST /api/v1/agents/templates/{slug}/instantiate — Create plan from template

Models & Migrations

  • Create AgentApiKey model with: (Phase 4)
    • workspace_id, name, key (hashed), permissions (JSON)
    • last_used_at, call_count, rate_limit
    • expires_at, revoked_at
    • Key methods: generate(), findByKey(), hasPermission(), revoke(), recordUsage()
  • Migration for agent_api_keys table (Phase 4)
  • Add api_key_id to agent_sessions table (track which key started session) (Phase 4)
  • Add api_key_id to mcp_tool_calls table (track which key made call) (Phase 4)

Services

  • Create AgentApiKeyService.php: (Phase 4)
    • create(), revoke(), validate()
    • checkPermission(), checkPermissions(), recordUsage()
    • isRateLimited(), getRateLimitStatus()
    • authenticate() — Full auth flow with rate limiting
    • updatePermissions(), updateRateLimit()
  • PlanTemplateService.php already has all needed methods (Phase 5)
    • list(), get(), createPlan(), previewTemplate()
    • getCategories(), getByCategory(), validateVariables()
    • YAML import handled directly in Livewire component

Sidebar Integration

  • Add "Agents" group to Hades sidebar:
    Agents
    ├── Dashboard      → hub.agents.index ✅
    ├── Plans          → hub.agents.plans ✅
    ├── Sessions       → hub.agents.sessions ✅ (Phase 2)
    ├── Tool Analytics → hub.agents.tools ✅ (Phase 3)
    ├── API Keys       → hub.agents.api-keys (Phase 4)
    └── Templates      → hub.agents.templates (Phase 5)
    
  • Sessions link added to sidebar (Phase 2)
  • Sessions quick link activated in dashboard (Phase 2)
  • Add badge for plans needing attention (blocked phases)
  • Tool Analytics link added to sidebar (Phase 3)
  • Tool Analytics quick link activated in dashboard (Phase 3)
  • API Keys link added to sidebar (Phase 4)
  • Templates link added to sidebar (Phase 5)
  • Templates quick link activated in dashboard (Phase 5)

Testing

  • Feature test: Plan list filters work
  • Feature test: Plan status transitions work
  • Feature test: Session creation and update work
  • Feature test: API key authentication works
  • Feature test: API key permissions enforced
  • Feature test: Rate limiting works
  • Feature test: Template instantiation works
  • Browser test: Dashboard loads with charts
  • Browser test: Real-time session updates work

UI Specifications

Agent Dashboard (Overview)

┌─────────────────────────────────────────────────────────────────┐
│ Agent Operations                                      [Refresh] │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
│  │ 12       │  │ 3        │  │ 847      │  │ 98.2%    │       │
│  │ Active   │  │ Sessions │  │ Tool     │  │ Success  │       │
│  │ Plans    │  │ Today    │  │ Calls    │  │ Rate     │       │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘       │
│                                                                 │
│  Recent Activity                     Tool Usage (7 days)       │
│  ┌─────────────────────────┐        ┌─────────────────────┐   │
│  │ • Plan "content-gen"    │        │ ▓▓▓▓▓▓▓▓░░ 80%     │   │
│  │   Phase 2 completed     │        │ content_create      │   │
│  │   2 minutes ago         │        │                     │   │
│  │                         │        │ ▓▓▓▓▓░░░░░ 50%     │   │
│  │ • Session opus-abc123   │        │ content_read        │   │
│  │   Started work on plan  │        │                     │   │
│  │   5 minutes ago         │        │ ▓▓▓░░░░░░░ 30%     │   │
│  └─────────────────────────┘        │ search_docs         │   │
│                                      └─────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

Plan Detail View

┌─────────────────────────────────────────────────────────────────┐
│ Plan: Content Migration                           Status: Active│
│ Workspace: main                                                 │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ Progress: ████████████░░░░░░░░ 60%                             │
│                                                                 │
│ Phases:                                                         │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ ✓ Phase 1: Audit Existing Content          [Completed]      ││
│ │   └─ 5/5 tasks complete                                     ││
│ ├─────────────────────────────────────────────────────────────┤│
│ │ ● Phase 2: Migrate Blog Posts              [In Progress]    ││
│ │   ├─ ✓ Export WordPress posts                               ││
│ │   ├─ ✓ Transform to native format                           ││
│ │   ├─ ○ Import to ContentItem                                ││
│ │   └─ ○ Verify all posts render                              ││
│ │   Progress: ██████░░░░ 50%                                  ││
│ ├─────────────────────────────────────────────────────────────┤│
│ │ ○ Phase 3: Migrate Help Articles           [Pending]        ││
│ │   └─ Waiting for Phase 2                                    ││
│ └─────────────────────────────────────────────────────────────┘│
│                                                                 │
│ Sessions:                                                       │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ sess_abc123 │ opus │ Active │ Started 10m ago │ [View]      ││
│ │ sess_xyz789 │ sonnet │ Completed │ 2h duration │ [View]     ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘

Session Detail View

┌─────────────────────────────────────────────────────────────────┐
│ Session: sess_abc123                              Agent: opus   │
│ Plan: Content Migration                        Status: Active   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│ Duration: 10m 23s                    Last Active: 30s ago      │
│                                                                 │
│ Work Log:                                                       │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ 15:30:00 [info] Starting phase 2 execution                  ││
│ │ 15:30:15 [success] Exported 47 WordPress posts              ││
│ │ 15:32:00 [info] Transforming post format...                 ││
│ │ 15:35:22 [checkpoint] 25/47 posts transformed               ││
│ │ 15:38:45 [success] All posts transformed                    ││
│ │ 15:39:00 [info] Beginning import to ContentItem...          ││
│ └─────────────────────────────────────────────────────────────┘│
│                                                                 │
│ Artifacts:                                                      │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ storage/exports/wp-posts.json    │ created │ 15:30:15      ││
│ │ app/Services/ContentImporter.php │ modified │ 15:35:22     ││
│ └─────────────────────────────────────────────────────────────┘│
│                                                                 │
│ Handoff Notes:                                                  │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ Summary: Completed export and transformation phases.        ││
│ │ Next: Import remaining 22 posts, verify rendering.          ││
│ │ Blockers: None currently.                                   ││
│ └─────────────────────────────────────────────────────────────┘│
│                                                                 │
│ [Pause Session] [Complete Session] [Fail Session]              │
└─────────────────────────────────────────────────────────────────┘

API Authentication

Key Generation

// Create key
$key = AgentApiKey::create([
    'workspace_id' => $workspace->id,
    'name' => 'Claude Code Integration',
    'permissions' => ['plans.read', 'plans.write', 'sessions.write'],
    'rate_limit' => 100, // per minute
    'expires_at' => now()->addYear(),
]);

// Returns unhashed key ONCE for user to copy
return $key->plainTextKey; // ak_live_xxxxxxxxxxxx

Key Usage

# External agent creates a plan
curl -X POST https://host.uk.com/api/v1/agents/plans \
  -H "Authorization: Bearer ak_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "content-migration",
    "variables": {
      "source": "wordpress",
      "workspace": "main"
    }
  }'

# Agent logs work to session
curl -X PATCH https://host.uk.com/api/v1/agents/sessions/sess_abc123 \
  -H "Authorization: Bearer ak_live_xxxxxxxxxxxx" \
  -d '{
    "work_log": {
      "type": "checkpoint",
      "message": "Completed 25/47 posts"
    }
  }'

Permissions

Permission Allows
plans.read List and view plans
plans.write Create, update, archive plans
phases.write Update phase status, add/complete tasks
sessions.read List and view sessions
sessions.write Start, update, complete sessions
tools.read View tool analytics
templates.read List and view templates
templates.instantiate Create plans from templates

Dependencies

  • Flux UI components (cards, tables, charts)
  • Chart library for analytics (ApexCharts or Chart.js via Livewire)
  • Real-time updates (Livewire polling or Laravel Echo)
  • Existing agent models (AgentPlan, AgentPhase, AgentSession)
  • Existing MCP models (McpToolCall, McpToolCallStat)

Integration with Existing Infrastructure

MCP Registry

The existing MCP registry at mcp.host.uk.com provides agent discovery. The new API endpoints extend this with:

  • Plan management (create, track, complete)
  • Session logging (work log, artifacts, handoff)
  • State persistence (shared workspace state)

Entitlement Gating

Future: Add entitlement features for agent access:

  • agents.plans.create — Can create agent plans
  • agents.sessions.concurrent — Max concurrent sessions
  • agents.tools.calls — Monthly tool call limit
  • agents.api_keys — Max API keys per workspace

External Agent Flow

External Agent (Claude Code, Cursor, etc.)
    │
    ├─ Discovers Host Hub via mcp.host.uk.com
    │
    ├─ Authenticates with API key
    │
    ├─ Creates plan from template
    │   POST /api/v1/agents/plans
    │
    ├─ Starts session
    │   POST /api/v1/agents/sessions
    │
    ├─ Logs work progress
    │   PATCH /api/v1/agents/sessions/{id}
    │
    ├─ Completes tasks
    │   PATCH /api/v1/agents/plans/{slug}/phases/{id}/tasks/{name}
    │
    └─ Completes session with handoff notes
        POST /api/v1/agents/sessions/{id}/complete

Notes

Why API Keys (Not OAuth)

For agent-to-agent communication:

  • OAuth is designed for human authorization flows
  • API keys are simpler for programmatic access
  • Keys can be scoped per workspace and permission
  • Rate limiting is straightforward
  • Revocation is immediate

Real-Time Considerations

For session monitoring:

  • Start with Livewire polling (every 5 seconds)
  • Upgrade to WebSocket (Laravel Echo) if needed
  • Consider SSE for one-way updates (server → browser)

Multi-System Cooperation

The API enables:

  1. Claude Code creates plan for Host Hub work
  2. Cursor picks up session for code changes
  3. Host Hub AI generates content as directed
  4. Human reviews in admin UI, approves or redirects

Each system maintains its own context but shares state via the plan.


Verification Results

Phase 1 Verification (2026-01-02 19:30)

Verification Agent: Claude Opus 4.5 Verdict: PASS - All Phase 1 acceptance criteria met

AC1: Route hub.agents.plans shows all AgentPlans (filterable by workspace, status)

Method: Code inspection of routes/web.php and app/Livewire/Admin/Agent/Plans.php

Evidence - Routes (lines 180-185):

Route::prefix('agents')->name('agents.')->group(function () {
    Route::get('/', \App\Livewire\Admin\Agent\Dashboard::class)->name('index');
    Route::get('/plans', \App\Livewire\Admin\Agent\Plans::class)->name('plans');
    Route::get('/plans/{slug}', \App\Livewire\Admin\Agent\PlanDetail::class)->name('plans.show');
});

Evidence - Plans.php (lines 39-61):

public function plans(): LengthAwarePaginator
{
    $query = AgentPlan::with(['workspace', 'agentPhases'])
        ->withCount('sessions');

    if ($this->search) { ... }
    if ($this->status) { $query->where('status', $this->status); }
    if ($this->workspace) { $query->where('workspace_id', $this->workspace); }

    return $query->latest('updated_at')->paginate($this->perPage);
}

Filters confirmed: Search (title/slug/description), Status (draft/active/completed/archived), Workspace

AC2: Plan list shows: title, workspace, status, phase progress, last activity

Method: Code inspection of resources/views/admin/livewire/agent/plans.blade.php

Evidence - Table headers (lines 44-53):

<th>Plan</th>           <!-- title + slug -->
<th>Workspace</th>      <!-- workspace name -->
<th>Status</th>         <!-- badge with status -->
<th>Progress</th>       <!-- visual bar + percentage -->
<th>Sessions</th>       <!-- session count -->
<th>Last Activity</th>  <!-- updated_at->diffForHumans() -->

All required columns present:

  • Title (line 64): {{ $plan->title }}
  • Workspace (line 69): {{ $plan->workspace?->name ?? 'N/A' }}
  • Status (lines 71-86): Coloured badge with ucfirst status
  • Progress (lines 88-96): Visual bar + percentage + phases count
  • Last Activity (lines 100-102): {{ $plan->updated_at->diffForHumans() }}

AC3: Plan detail view shows all phases with task checklists

Method: Code inspection of resources/views/admin/livewire/agent/plan-detail.blade.php

Evidence - Phases section (lines 66-183):

<flux:card class="p-6 mb-6">
    <flux:heading size="lg" class="mb-4">Phases</flux:heading>
    @foreach($this->phases as $phase)
        <!-- Phase header with name, status, progress -->
        @if($phase->tasks && count($phase->tasks) > 0)
            <div class="p-4 space-y-2">
                @foreach($phase->tasks as $index => $task)
                    <!-- Task checkbox with name and optional notes -->
                @endforeach
            </div>
        @endif
    @endforeach
</flux:card>

Task checklist features:

  • Checkbox toggle for completing tasks (line 143-156)
  • Completed tasks show strikethrough (line 158)
  • Optional task notes displayed (lines 159-161)
  • "Add Task" modal (lines 248-274)

AC4: Phase progress displayed as visual percentage bar

Method: Code inspection of views

Evidence - Plan-level progress (lines 29-56):

<div class="w-full bg-zinc-200 dark:bg-zinc-700 rounded-full h-4 mb-4">
    <div class="bg-violet-500 h-4 rounded-full" style="width: {{ $this->progress['percentage'] }}%"></div>
</div>

Evidence - Phase-level progress (lines 96-102):

@if($taskProgress['total'] > 0)
    <div class="w-20 bg-zinc-200 dark:bg-zinc-700 rounded-full h-2">
        <div class="bg-violet-500 h-2 rounded-full" style="width: {{ $taskProgress['percentage'] }}%"></div>
    </div>
    <flux:text size="sm">{{ $taskProgress['completed'] }}/{{ $taskProgress['total'] }}</flux:text>
@endif

Progress bars present at:

  • Plan list (each row has progress bar + percentage)
  • Plan detail (overview card with large progress bar)
  • Each phase (task completion progress)

AC5: Can manually update plan status (activate, complete, archive)

Method: Code inspection of Plans.php and PlanDetail.php

Evidence - Plans.php (lines 103-122):

public function activate(int $planId): void {
    $plan = AgentPlan::findOrFail($planId);
    $plan->activate();
}

public function complete(int $planId): void {
    $plan = AgentPlan::findOrFail($planId);
    $plan->complete();
}

public function archive(int $planId): void {
    $plan = AgentPlan::findOrFail($planId);
    $plan->archive('Archived via admin UI');
}

Evidence - PlanDetail.php (lines 55-72):

public function activatePlan(): void { $this->plan->activate(); }
public function completePlan(): void { $this->plan->complete(); }
public function archivePlan(): void { $this->plan->archive('Archived via admin UI'); }

UI buttons confirmed:

  • Plans list: Dropdown menu with Activate/Complete/Archive/Delete (lines 108-122)
  • Plan detail: Header buttons for Activate/Complete/Archive (lines 17-26)

AC6: Can manually update phase status (start, complete, block, skip)

Method: Code inspection of PlanDetail.php

Evidence - Phase actions (lines 75-120):

public function startPhase(int $phaseId): void {
    $phase = AgentPhase::findOrFail($phaseId);
    if (! $phase->canStart()) { return; }
    $phase->start();
}

public function completePhase(int $phaseId): void {
    $phase = AgentPhase::findOrFail($phaseId);
    $phase->complete();
}

public function blockPhase(int $phaseId): void {
    $phase = AgentPhase::findOrFail($phaseId);
    $phase->block('Blocked via admin UI');
}

public function skipPhase(int $phaseId): void {
    $phase = AgentPhase::findOrFail($phaseId);
    $phase->skip('Skipped via admin UI');
}

public function resetPhase(int $phaseId): void {
    $phase = AgentPhase::findOrFail($phaseId);
    $phase->reset();
}

Evidence - View dropdown (lines 106-128):

<flux:menu>
    @if($phase->isPending())
        <flux:menu.item wire:click="startPhase(...)">Start Phase</flux:menu.item>
    @endif
    @if($phase->isInProgress())
        <flux:menu.item wire:click="completePhase(...)">Complete Phase</flux:menu.item>
        <flux:menu.item wire:click="blockPhase(...)">Block Phase</flux:menu.item>
    @endif
    @if($phase->isBlocked())
        <flux:menu.item wire:click="resetPhase(...)">Unblock (Reset)</flux:menu.item>
    @endif
    @if(!$phase->isCompleted() && !$phase->isSkipped())
        <flux:menu.item wire:click="skipPhase(...)">Skip Phase</flux:menu.item>
    @endif
    @if($phase->isCompleted() || $phase->isSkipped())
        <flux:menu.item wire:click="resetPhase(...)">Reset to Pending</flux:menu.item>
    @endif
</flux:menu>

All phase status actions available: Start, Complete, Block, Skip, Reset

Phase 1 Summary

AC Description Evidence Status
AC1 Route exists with filters routes/web.php:184, Plans.php query builder PASS
AC2 Plan list columns plans.blade.php table with all 6 columns PASS
AC3 Phases with task checklists plan-detail.blade.php:132-165 task list with checkboxes PASS
AC4 Visual progress bars Plan list + detail + per-phase progress PASS
AC5 Plan status updates activate(), complete(), archive() methods PASS
AC6 Phase status updates startPhase(), completePhase(), blockPhase(), skipPhase(), resetPhase() PASS

Additional Implementation Verified:

  • Hades access control in all components (checkHadesAccess() in mount)
  • Sidebar integration (lines 520-576 in sidebar.blade.php)
  • Dashboard with stats and recent activity (Dashboard.php)
  • Task management (add task modal, complete task)
  • Session list in plan detail view

Phase 1 Status: VERIFIED — Ready for human approval


Phase 2 Verification (2026-01-02 20:00)

Verification Agent: Claude Opus 4.5 Verdict: PASS - All Phase 2 acceptance criteria met

AC7: Route hub.agents.sessions shows all AgentSessions

Method: Code inspection of routes/web.php and app/Livewire/Admin/Agent/Sessions.php

Evidence - Routes (lines 187-188):

Route::get('/sessions', \App\Livewire\Admin\Agent\Sessions::class)->name('sessions');
Route::get('/sessions/{id}', \App\Livewire\Admin\Agent\SessionDetail::class)->name('sessions.show');

Evidence - Sessions.php (lines 46-76):

public function sessions(): LengthAwarePaginator
{
    $query = AgentSession::with(['workspace', 'plan']);

    if ($this->search) { ... }
    if ($this->status) { $query->where('status', $this->status); }
    if ($this->agentType) { $query->where('agent_type', $this->agentType); }
    if ($this->workspace) { $query->where('workspace_id', $this->workspace); }
    if ($this->planSlug) { $query->whereHas('plan', ...); }

    return $query->latest('last_active_at')->paginate($this->perPage);
}

Filters confirmed: Search, Status, Agent Type, Workspace, Plan

AC8: Active sessions show real-time activity (polling or WebSocket)

Method: Code inspection of SessionDetail.php and session-detail.blade.php

Evidence - Polling implementation (SessionDetail.php lines 27-41, 104-114):

public int $pollingInterval = 5000;

public function mount(int $id): void
{
    // Disable polling for completed/failed sessions
    if ($this->session->isEnded()) {
        $this->pollingInterval = 0;
    }
}

public function poll(): void
{
    $this->session->refresh();
    if ($this->session->isEnded()) {
        $this->pollingInterval = 0;
    }
}

Evidence - View polling directive (session-detail.blade.php line 1):

<div wire:poll.{{ $pollingInterval }}ms="poll">

Polling behaviour:

  • 5-second interval for active sessions
  • Automatically disabled when session ends
  • Refreshes session data on each poll

AC9: Session detail shows: work log, artifacts, handoff notes

Method: Code inspection of session-detail.blade.php

Evidence - Work Log (lines 143-188):

<flux:card>
    <div class="p-4 border-b">
        <flux:heading size="sm">Work Log</flux:heading>
    </div>
    @foreach($this->recentWorkLog as $entry)
        <!-- Entry with action, type, details, timestamp -->
    @endforeach
</flux:card>

Evidence - Artifacts (lines 228-257):

<flux:card>
    <flux:heading size="sm">Artifacts</flux:heading>
    @foreach($this->artifacts as $artifact)
        <!-- Artifact name, type, path -->
    @endforeach
</flux:card>

Evidence - Handoff Notes (lines 259-296):

<flux:card>
    <flux:heading size="sm">Handoff Notes</flux:heading>
    <!-- Summary, blockers, suggested next agent -->
</flux:card>

All three sections present with proper data display

AC10: Can view session context summary and final summary

Method: Code inspection of SessionDetail.php and session-detail.blade.php

Evidence - Context Summary computed property (lines 69-73):

#[Computed]
public function contextSummary(): ?array
{
    return $this->session->context_summary;
}

Evidence - Context Summary view (lines 110-141):

@if($this->contextSummary)
    <flux:card>
        <flux:heading size="sm">Context Summary</flux:heading>
        <!-- Goal, Progress, Next Steps -->
    </flux:card>
@endif

Evidence - Final Summary view (lines 190-200):

@if($session->final_summary)
    <flux:card>
        <flux:heading size="sm">Final Summary</flux:heading>
        <flux:text class="whitespace-pre-wrap">{{ $session->final_summary }}</flux:text>
    </flux:card>
@endif

Both summary types displayed when present

AC11: Timeline view shows session sequence within a plan

Method: Code inspection of SessionDetail.php and session-detail.blade.php

Evidence - Plan Sessions computed property (lines 75-85):

#[Computed]
public function planSessions(): Collection
{
    if (! $this->session->agent_plan_id) {
        return collect();
    }
    return AgentSession::where('agent_plan_id', $this->session->agent_plan_id)
        ->orderBy('started_at')
        ->get();
}

Evidence - Session Index (lines 87-102):

#[Computed]
public function sessionIndex(): int
{
    // Returns 1-based index of current session in plan
}

Evidence - Timeline view (lines 71-105):

@if($session->agent_plan_id && $this->planSessions->count() > 1)
    <flux:card class="p-4 mb-6">
        <flux:heading size="sm">Plan Timeline (Session {{ $this->sessionIndex }} of {{ $this->planSessions->count() }})</flux:heading>
        <div class="flex items-center gap-2 overflow-x-auto">
            @foreach($this->planSessions as $index => $planSession)
                <!-- Clickable session card with number, agent type, date -->
                <!-- Current session highlighted, chevron separators -->
            @endforeach
        </div>
    </flux:card>
@endif

Timeline shows:

  • Session number in sequence (1 of N)
  • All sessions ordered by start time
  • Current session highlighted
  • Click to navigate to other sessions
  • Status indicators (active pulse, completed/failed colours)

AC12: Can pause/resume/fail active sessions manually

Method: Code inspection of Sessions.php, SessionDetail.php, and views

Evidence - Sessions.php list actions (lines 127-153):

public function pause(int $sessionId): void { $session->pause(); }
public function resume(int $sessionId): void { $session->resume(); }
public function complete(int $sessionId): void { $session->complete('...'); }
public function fail(int $sessionId): void { $session->fail('...'); }

Evidence - SessionDetail.php actions (lines 117-156):

public function pauseSession(): void { $this->session->pause(); }
public function resumeSession(): void { $this->session->resume(); }
public function completeSession(): void { $this->session->complete($this->completeSummary); }
public function failSession(): void { $this->session->fail($this->failReason); }

Evidence - Sessions list dropdown (sessions.blade.php lines 143-157):

<flux:menu>
    @if($session->isActive())
        <flux:menu.item wire:click="pause(...)">Pause</flux:menu.item>
    @endif
    @if($session->isPaused())
        <flux:menu.item wire:click="resume(...)">Resume</flux:menu.item>
    @endif
    <flux:menu.item wire:click="complete(...)">Complete</flux:menu.item>
    <flux:menu.item wire:click="fail(...)" variant="danger">Fail</flux:menu.item>
</flux:menu>

Evidence - Session detail buttons (session-detail.blade.php lines 28-39):

@if($session->isActive())
    <flux:button wire:click="pauseSession">Pause</flux:button>
@elseif($session->isPaused())
    <flux:button wire:click="resumeSession">Resume</flux:button>
@endif
@if(!$session->isEnded())
    <flux:button wire:click="openCompleteModal">Complete</flux:button>
    <flux:button wire:click="openFailModal" variant="danger">Fail</flux:button>
@endif

Complete/Fail modals (lines 300-324):

  • Complete modal with optional summary
  • Fail modal with reason input

Phase 2 Summary

AC Description Evidence Status
AC7 Sessions route with filters routes/web.php:187-188, Sessions.php query builder PASS
AC8 Real-time polling wire:poll.5000ms, disabled when session ends PASS
AC9 Work log, artifacts, handoff notes Three dedicated sections in session-detail.blade.php PASS
AC10 Context and final summary Computed properties + conditional display PASS
AC11 Timeline view Horizontal timeline with session sequence navigation PASS
AC12 Manual session controls Pause/Resume/Complete/Fail with modals PASS

Additional Implementation Verified:

  • Hades access control in both components
  • Sidebar link at line 558 (hub.agents.sessions)
  • Dashboard quick link at line 160
  • Active session count indicator with animated pulse
  • Agent type badges (Opus/Sonnet/Haiku)
  • Status colour coding

Phase 2 Status: VERIFIED — Ready for human approval


Phase 3 Verification (2026-01-02 21:45)

Verification Agent: Claude Opus 4.5 Verdict: PASS - All Phase 3 acceptance criteria met

AC13: Route hub.agents.tools shows MCP tool usage dashboard

Method: Code inspection of routes/web.php and app/Livewire/Admin/Agent/ToolAnalytics.php

Evidence - Routes (lines 189-191):

/ Phase 3: Tool Analytics
Route::get('/tools', \App\Livewire\Admin\Agent\ToolAnalytics::class)->name('tools');
Route::get('/tools/calls', \App\Livewire\Admin\Agent\ToolCalls::class)->name('tools.calls');

Evidence - ToolAnalytics.php (lines 17-18, 29-32):

#[Title('Tool Analytics')]
class ToolAnalytics extends Component
{
    public function mount(): void
    {
        $this->checkHadesAccess();
    }
}

Dashboard features:

  • Stats cards: Total calls, Successful, Errors, Success rate, Unique tools (lines 51-111 in view)
  • Filters: Days (7/14/30/90), Workspace, Server (lines 15-48 in view)
  • Hades access control enforced

AC14: Top 10 tools by usage displayed

Method: Code inspection of ToolAnalytics.php and tool-analytics.blade.php

Evidence - ToolAnalytics.php (lines 77-88):

#[Computed]
public function topTools(): Collection
{
    $workspaceId = $this->workspace ? (int) $this->workspace : null;
    $tools = McpToolCallStat::getTopTools($this->days, 10, $workspaceId);

    if ($this->server) {
        $tools = $tools->filter(fn ($t) => $t->server_id === $this->server)->values();
    }

    return $tools->take(10);
}

Evidence - View (lines 168-236):

<flux:heading size="lg">Top 10 Tools</flux:heading>
<table>
    <thead>
        <th>Tool</th><th>Server</th><th>Calls</th><th>Success Rate</th><th>Errors</th><th>Avg Duration</th>
    </thead>
    <tbody>
        @foreach($this->topTools as $tool)
            <!-- Tool row with drill-down link -->
        @endforeach
    </tbody>
</table>

Top 10 tools displayed with:

  • Tool name, Server ID
  • Total calls count
  • Success rate (colour-coded)
  • Error count
  • Average duration
  • Drill-down button

AC15: Daily trend chart (calls per day, 30-day window)

Method: Code inspection of ToolAnalytics.php and tool-analytics.blade.php

Evidence - ToolAnalytics.php (lines 90-96, 131-142):

#[Computed]
public function dailyTrend(): Collection
{
    $workspaceId = $this->workspace ? (int) $this->workspace : null;
    return McpToolCallStat::getDailyTrend($this->days, $workspaceId);
}

#[Computed]
public function chartData(): array
{
    $trend = $this->dailyTrend;
    return [
        'labels' => $trend->pluck('date')->map(fn ($d) => $d->format('M j'))->toArray(),
        'calls' => $trend->pluck('total_calls')->toArray(),
        'errors' => $trend->pluck('total_errors')->toArray(),
        'success_rates' => $trend->pluck('success_rate')->toArray(),
    ];
}

Evidence - Chart.js integration (lines 272-346):

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
Alpine.data('chartComponent', (chartData) => ({
    initChart() {
        new Chart(ctx, {
            type: 'line',
            data: {
                labels: this.chartData.labels,
                datasets: [
                    { label: 'Total Calls', data: this.chartData.calls },
                    { label: 'Errors', data: this.chartData.errors }
                ]
            }
        });
    }
}));
</script>

Chart features:

  • Line chart with Chart.js
  • Configurable window: 7/14/30/90 days (view line 19-24)
  • Shows Total Calls (violet) and Errors (red)
  • Dark mode support
  • Responsive design

AC16: Server breakdown shows usage by MCP server

Method: Code inspection of ToolAnalytics.php and tool-analytics.blade.php

Evidence - ToolAnalytics.php (lines 98-109):

#[Computed]
public function serverStats(): Collection
{
    $workspaceId = $this->workspace ? (int) $this->workspace : null;
    $stats = McpToolCallStat::getServerStats($this->days, $workspaceId);

    if ($this->server) {
        $stats = $stats->filter(fn ($s) => $s->server_id === $this->server)->values();
    }

    return $stats;
}

Evidence - View (lines 132-165):

<flux:heading size="lg">Server Breakdown</flux:heading>
@foreach($this->serverStats as $serverStat)
    <div class="space-y-2">
        <flux:text class="font-medium">{{ $serverStat->server_id }}</flux:text>
        <flux:text size="sm">{{ number_format($serverStat->total_calls) }} calls</flux:text>
        <div class="w-full bg-zinc-200 rounded-full h-2">
            <div class="bg-violet-500 h-2 rounded-full" style="width: {{ $percentage }}%"></div>
        </div>
        <flux:text size="xs">{{ $serverStat->unique_tools }} tools</flux:text>
        <flux:text size="xs">{{ $serverStat->success_rate }}% success</flux:text>
    </div>
@endforeach

Server breakdown shows:

  • Server ID
  • Total call count
  • Visual progress bar (relative to max)
  • Unique tools count
  • Success rate (colour-coded)

AC17: Error rate and average duration displayed per tool

Method: Code inspection of tool-analytics.blade.php

Evidence - Top Tools table columns (lines 183-218):

<th>Success Rate</th>
<th>Errors</th>
<th>Avg Duration</th>
...
<td>
    <flux:text class="{{ $this->getSuccessRateColorClass($tool->success_rate) }}">{{ $tool->success_rate }}%</flux:text>
</td>
<td>
    @if($tool->total_errors > 0)
        <span class="text-red-500 font-medium">{{ number_format($tool->total_errors) }}</span>
    @else
        <flux:text class="text-zinc-400">0</flux:text>
    @endif
</td>
<td>
    @if($tool->avg_duration)
        <flux:text size="sm">{{ round($tool->avg_duration) < 1000 ? round($tool->avg_duration) . 'ms' : round($tool->avg_duration / 1000, 2) . 's' }}</flux:text>
    @endif
</td>

Error/duration display features:

  • Success rate with colour coding (green ≥95%, amber ≥80%, red <80%)
  • Error count highlighted in red when >0
  • Duration formatted as ms or seconds
  • Recent Errors section (lines 238-269) shows last 10 failures

AC18: Can drill down to individual tool calls with full params

Method: Code inspection of ToolCalls.php and tool-calls.blade.php

Evidence - ToolCalls.php (lines 133-151):

#[Computed]
public function selectedCall(): ?McpToolCall
{
    if (! $this->selectedCallId) {
        return null;
    }
    return McpToolCall::with('workspace')->find($this->selectedCallId);
}

public function viewCall(int $id): void
{
    $this->selectedCallId = $id;
}

public function closeCallDetail(): void
{
    $this->selectedCallId = null;
}

Evidence - View modal (lines 155-244):

@if($selectedCall)
    <flux:modal wire:model.self="selectedCallId" class="max-w-4xl">
        <flux:heading size="lg">{{ $selectedCall->tool_name }}</flux:heading>
        <!-- Metadata: Duration, Agent Type, Workspace, Time -->
        <!-- Session ID -->
        <!-- Plan link -->

        {{-- Input Parameters --}}
        @if($selectedCall->input_params && count($selectedCall->input_params) > 0)
            <div class="bg-zinc-100 rounded-lg p-4">
                <pre>{{ json_encode($selectedCall->input_params, JSON_PRETTY_PRINT) }}</pre>
            </div>
        @endif

        {{-- Error Details --}}
        @if(!$selectedCall->success)
            <div class="p-4 bg-red-50 border border-red-200 rounded-lg">
                <flux:text>Code: {{ $selectedCall->error_code }}</flux:text>
                <flux:text>{{ $selectedCall->error_message }}</flux:text>
            </div>
        @endif

        {{-- Result Summary --}}
        @if($selectedCall->result_summary && count($selectedCall->result_summary) > 0)
            <pre>{{ json_encode($selectedCall->result_summary, JSON_PRETTY_PRINT) }}</pre>
        @endif
    </flux:modal>
@endif

Drill-down features:

  • Modal with full tool call details
  • Input parameters (JSON pretty-printed)
  • Result summary (JSON pretty-printed)
  • Error details (code + message) for failed calls
  • Metadata: duration, agent type, workspace, timestamp
  • Session and Plan links for traceability
  • Filters: server, tool, status, workspace, agent type
  • Pagination with 25 per page

Phase 3 Summary

AC Description Evidence Status
AC13 Tool usage dashboard route routes/web.php:190, ToolAnalytics.php with Hades auth PASS
AC14 Top 10 tools by usage topTools() computed, table with 6 columns PASS
AC15 Daily trend chart Chart.js line chart, configurable 7-90 days PASS
AC16 Server breakdown serverStats() computed, progress bars, success rates PASS
AC17 Error rate and duration Table columns + colour-coded success rate PASS
AC18 Drill-down to tool calls Modal with input_params, result_summary, error_details PASS

Additional Implementation Verified:

  • Hades access control in both components
  • Sidebar link at hub.agents.tools
  • Dashboard quick link to Tool Analytics
  • Drill-down links from Top Tools table
  • Recent Errors section in dashboard
  • Comprehensive filtering in ToolCalls
  • Pagination with proper links

Phase 3 Status: VERIFIED — Ready for human approval


Phase 3 Implementation Notes (2026-01-02 21:30)

Implementation Agent: Claude Opus 4.5

Routes Added (routes/web.php):

  • hub.agents.tools → ToolAnalytics dashboard
  • hub.agents.tools.calls → ToolCalls drill-down list

Livewire Components Created:

  • app/Livewire/Admin/Agent/ToolAnalytics.php - Dashboard with stats, charts, filters
  • app/Livewire/Admin/Agent/ToolCalls.php - Paginated call list with modal detail

Views Created:

  • resources/views/admin/livewire/agent/tool-analytics.blade.php
  • resources/views/admin/livewire/agent/tool-calls.blade.php

Phase 5: Not yet implemented (commented out in routes)


Phase 4 Implementation Notes (2026-01-02 22:30)

Implementation Agent: Claude Opus 4.5

Route Added (routes/web.php):

  • hub.agents.api-keys → ApiKeys management

Model Created:

  • app/Models/Agent/AgentApiKey.php
    • Key generation with ak_ prefix + 32 random chars
    • SHA-256 hashing for storage
    • Permission constants: PERM_PLANS_READ, PERM_PLANS_WRITE, PERM_PHASES_WRITE, PERM_SESSIONS_READ, PERM_SESSIONS_WRITE, PERM_TOOLS_READ, PERM_TEMPLATES_READ, PERM_TEMPLATES_INSTANTIATE
    • Status helpers: isActive(), isRevoked(), isExpired()
    • Scopes: active(), revoked(), expired(), forWorkspace()

Migration Created:

  • database/migrations/2026_01_02_220000_create_agent_api_keys_table.php
    • Creates agent_api_keys table with key, permissions (JSON), rate_limit, call_count, timestamps
    • Adds agent_api_key_id foreign key to agent_sessions and mcp_tool_calls

Service Created:

  • app/Services/Agent/AgentApiKeyService.php
    • create() — Generate new key with permissions and rate limit
    • validate() — Validate plaintext key and return model
    • checkPermission(), checkPermissions() — Verify access
    • recordUsage() — Increment call count and cache for rate limiting
    • isRateLimited(), getRateLimitStatus() — Rate limiting helpers
    • authenticate() — Full auth flow returning structured result
    • revoke(), updatePermissions(), updateRateLimit()

Livewire Component Created:

  • app/Livewire/Admin/Agent/ApiKeys.php
    • Stats: total, active, revoked, total calls
    • Filters: workspace, status
    • Create modal: name, workspace, permissions, rate limit, expiry
    • Created key modal: shows plaintext key ONCE
    • Edit modal: update permissions and rate limit
    • Revoke action with immediate effect

View Created:

  • resources/views/admin/livewire/agent/api-keys.blade.php

Sidebar Updated:

  • Added API Keys link to Agents submenu

Phase 4 Status: COMPLETE — Ready for verification


Phase 4 Verification (2026-01-02 23:15)

Verification Agent: Claude Opus 4.5 Verdict: PASS - All Phase 4 acceptance criteria met

AC19: Route hub.agents.api-keys manages agent API access

Method: Code inspection of routes/web.php and app/Livewire/Admin/Agent/ApiKeys.php

Evidence - Route (routes/web.php line 193):

Route::get('/api-keys', \App\Livewire\Admin\Agent\ApiKeys::class)->name('api-keys');

Evidence - Sidebar link (sidebar.blade.php line 568):

<a class="..." href="{{ route('hub.agents.api-keys') }}">API Keys</a>

Evidence - Hades access control (ApiKeys.php lines 49-55):

public function mount(): void
{
    $this->checkHadesAccess();
}

private function checkHadesAccess(): void
{
    if (! auth()->user()?->isHades()) {
        abort(403, 'Hades access required');
    }
}

Route exists and is accessible only to Hades users

AC20: Can create API keys scoped to workspace

Method: Code inspection of ApiKeys.php and api-keys.blade.php

Evidence - ApiKeys.php createKey() (lines 89-116):

public function createKey(): void
{
    $this->validate([
        'newKeyName' => 'required|string|max:255',
        'newKeyWorkspace' => 'required|exists:workspaces,id',
        'newKeyPermissions' => 'array',
        'newKeyRateLimit' => 'required|integer|min:1|max:10000',
    ]);

    $key = $this->apiKeyService->create(
        workspace: (int) $this->newKeyWorkspace,
        name: $this->newKeyName,
        permissions: $this->newKeyPermissions,
        rateLimit: $this->newKeyRateLimit,
        expiresAt: $this->newKeyExpiry ? Carbon::parse($this->newKeyExpiry) : null
    );

    $this->createdKey = $key->plainTextKey;
    // ...
}

Evidence - View workspace dropdown (api-keys.blade.php lines 211-221):

<flux:select wire:model="newKeyWorkspace" label="Workspace">
    <flux:select.option value="">Select workspace...</flux:select.option>
    @foreach($this->workspaces as $ws)
        <flux:select.option value="{{ $ws->id }}">{{ $ws->name }}</flux:select.option>
    @endforeach
</flux:select>

Evidence - AgentApiKey model (lines 113-118):

public function scopeForWorkspace($query, Workspace|int $workspace)
{
    $workspaceId = $workspace instanceof Workspace ? $workspace->id : $workspace;
    return $query->where('workspace_id', $workspaceId);
}

Keys are scoped to workspace via foreign key and workspace selector

AC21: Keys have configurable permissions (read plans, write plans, execute tools)

Method: Code inspection of AgentApiKey.php and view

Evidence - Permission constants (AgentApiKey.php lines 63-78):

public const PERM_PLANS_READ = 'plans.read';
public const PERM_PLANS_WRITE = 'plans.write';
public const PERM_PHASES_WRITE = 'phases.write';
public const PERM_SESSIONS_READ = 'sessions.read';
public const PERM_SESSIONS_WRITE = 'sessions.write';
public const PERM_TOOLS_READ = 'tools.read';
public const PERM_TEMPLATES_READ = 'templates.read';
public const PERM_TEMPLATES_INSTANTIATE = 'templates.instantiate';

Evidence - availablePermissions() (lines 83-95):

public static function availablePermissions(): array
{
    return [
        self::PERM_PLANS_READ => 'List and view plans',
        self::PERM_PLANS_WRITE => 'Create, update, archive plans',
        self::PERM_PHASES_WRITE => 'Update phase status, add/complete tasks',
        self::PERM_SESSIONS_READ => 'List and view sessions',
        self::PERM_SESSIONS_WRITE => 'Start, update, complete sessions',
        self::PERM_TOOLS_READ => 'View tool analytics',
        self::PERM_TEMPLATES_READ => 'List and view templates',
        self::PERM_TEMPLATES_INSTANTIATE => 'Create plans from templates',
    ];
}

Evidence - View permission checkboxes (api-keys.blade.php lines 227-237):

<div class="space-y-2">
    @foreach(\Mod\Agentic\Models\AgentApiKey::availablePermissions() as $perm => $description)
        <label class="flex items-center gap-2">
            <flux:checkbox wire:model="newKeyPermissions" value="{{ $perm }}" />
            <span class="text-sm">{{ $description }}</span>
        </label>
    @endforeach
</div>

Evidence - Permission helpers (AgentApiKey.php lines 195-220):

public function hasPermission(string $permission): bool
public function hasAnyPermission(array $permissions): bool
public function hasAllPermissions(array $permissions): bool

8 configurable permissions with checkboxes in create/edit modals

AC22: Key usage tracking (last used, call count)

Method: Code inspection of model, service, and view

Evidence - Migration fields (migration lines 24-25):

$table->unsignedBigInteger('call_count')->default(0);
$table->timestamp('last_used_at')->nullable();

Evidence - AgentApiKey recordUsage() (lines 230-236):

public function recordUsage(): self
{
    $this->increment('call_count');
    $this->update(['last_used_at' => now()]);
    return $this;
}

Evidence - AgentApiKeyService recordUsage() (lines 79-91):

public function recordUsage(AgentApiKey $key): void
{
    $key->recordUsage();
    // Increment rate limit counter in cache
    $cacheKey = $this->getRateLimitCacheKey($key);
    Cache::increment($cacheKey);
    // ...
}

Evidence - View table columns (api-keys.blade.php lines 106-118):

<td>{{ number_format($key->call_count) }}</td>
<td>{{ $key->getLastUsedForHumans() }}</td>

Evidence - Stats card (api-keys.blade.php lines 54-59):

<flux:text class="text-2xl font-semibold">{{ number_format($this->stats['total_calls']) }}</flux:text>
<flux:text size="sm" class="text-zinc-500">Total Calls</flux:text>

Usage tracked via call_count + last_used_at, displayed in table and stats

AC23: Can revoke keys immediately

Method: Code inspection of service, model, and Livewire component

Evidence - AgentApiKey revoke() (lines 223-228):

public function revoke(): self
{
    $this->update(['revoked_at' => now()]);
    return $this;
}

Evidence - AgentApiKeyService revoke() (lines 129-135):

public function revoke(AgentApiKey $key): void
{
    $key->revoke();
    // Clear rate limit cache
    Cache::forget($this->getRateLimitCacheKey($key));
}

Evidence - ApiKeys.php revokeKey() (lines 118-128):

public function revokeKey(int $keyId): void
{
    $key = AgentApiKey::findOrFail($keyId);
    $this->apiKeyService->revoke($key);
    Flux::toast(
        heading: 'API Key Revoked',
        text: "Key '{$key->name}' has been revoked and can no longer be used.",
        variant: 'warning',
    );
}

Evidence - View revoke button (api-keys.blade.php line 133):

<flux:menu.item wire:click="revokeKey({{ $key->id }})" wire:confirm="Revoke this API key? This action cannot be undone." variant="danger">
    Revoke Key
</flux:menu.item>

Evidence - isRevoked helper (AgentApiKey.php lines 184-187):

public function isRevoked(): bool
{
    return $this->revoked_at !== null;
}

Revocation sets revoked_at timestamp, clears cache, shows confirmation

AC24: Rate limiting configuration per key

Method: Code inspection of model, service, and view

Evidence - Migration field (line 23):

$table->integer('rate_limit')->default(100);     // Calls per minute

Evidence - AgentApiKeyService rate limiting (lines 96-124):

public function isRateLimited(AgentApiKey $key): bool
{
    $cacheKey = $this->getRateLimitCacheKey($key);
    $currentCalls = (int) Cache::get($cacheKey, 0);
    return $currentCalls >= $key->rate_limit;
}

public function getRateLimitStatus(AgentApiKey $key): array
{
    $cacheKey = $this->getRateLimitCacheKey($key);
    $currentCalls = (int) Cache::get($cacheKey, 0);
    $remaining = max(0, $key->rate_limit - $currentCalls);
    // ...
    return [
        'limit' => $key->rate_limit,
        'remaining' => $remaining,
        'reset_in_seconds' => max(0, $ttl),
        'used' => $currentCalls,
    ];
}

Evidence - authenticate() rate limiting check (lines 253-262):

if ($this->isRateLimited($key)) {
    $status = $this->getRateLimitStatus($key);
    return [
        'success' => false,
        'error' => 'rate_limited',
        'message' => 'Rate limit exceeded',
        'rate_limit' => $status,
    ];
}

Evidence - View rate limit input (api-keys.blade.php lines 239-241):

<flux:input type="number" wire:model="newKeyRateLimit" label="Rate Limit (calls/min)" min="1" max="10000" />

Evidence - Table column (api-keys.blade.php line 102):

<td>{{ $key->rate_limit }}/min</td>

Evidence - Edit modal rate limit (api-keys.blade.php lines 334-336):

<flux:input type="number" wire:model="editKeyRateLimit" label="Rate Limit (calls/min)" min="1" max="10000" />

Rate limit configurable 1-10000 calls/min, enforced via Cache with 60s window

Phase 4 Summary

AC Description Evidence Status
AC19 Route manages API access routes/web.php:193, Hades auth, sidebar link PASS
AC20 Keys scoped to workspace workspace_id FK, workspace dropdown in create modal PASS
AC21 Configurable permissions 8 permissions with checkboxes, hasPermission() helpers PASS
AC22 Usage tracking call_count + last_used_at fields, displayed in UI PASS
AC23 Immediate revocation revoke() sets timestamp, clears cache, confirmation dialog PASS
AC24 Rate limiting per key Configurable 1-10000/min, Cache-based enforcement PASS

Additional Implementation Verified:

  • Hades access control in component
  • Sidebar link at line 568
  • Stats cards: total, active, revoked, total calls
  • Create modal shows plaintext key ONCE
  • Edit modal for permissions and rate limit updates
  • Key expiry support (optional)
  • Filter by workspace and status (active/revoked/expired)
  • Foreign keys added to agent_sessions and mcp_tool_calls for tracking

Phase 4 Status: VERIFIED — Ready for human approval


Phase 5 Implementation Notes (2026-01-02 23:45)

Implementation Agent: Claude Opus 4.5

Route Added (routes/web.php):

  • hub.agents.templates → Templates browser

Livewire Component Created:

  • app/Livewire/Admin/Agent/Templates.php
    • Uses existing PlanTemplateService for all template operations
    • Template listing with category filter and search
    • Preview modal showing phases, tasks, variables, guidelines
    • Create plan modal with variable inputs and workspace selection
    • YAML import modal with file upload, validation, and preview
    • Delete template functionality

View Created:

  • resources/views/admin/livewire/agent/templates.blade.php
    • Stats cards: total templates, categories, total phases, with variables
    • Grid layout for template cards
    • Category colour coding
    • Preview modal with full template details
    • Create modal with variable input fields
    • Import modal with drag-drop file upload

Sidebar Updated:

  • Added Templates link to Agents submenu

Dashboard Updated:

  • Templates quick link now active (was "Coming soon")

Existing Templates (5):

  • bug-fix.yaml - Bug fix workflow
  • code-review.yaml - Code review process
  • feature-port.yaml - Feature porting
  • new-feature.yaml - New feature development
  • refactor.yaml - Code refactoring

Phase 5 Status: COMPLETE — Ready for verification


Phase 5 Verification (2026-01-02 23:45)

Verification Agent: Claude Opus 4.5 Verdict: PASS - All Phase 5 acceptance criteria met

AC25: Route hub.agents.templates lists available plan templates

Method: Code inspection of routes/web.php and app/Livewire/Admin/Agent/Templates.php

Evidence - Route (routes/web.php line 195):

Route::get('/templates', \App\Livewire\Admin\Agent\Templates::class)->name('templates');

Evidence - Sidebar link (sidebar.blade.php lines 573-574):

<a class="..." href="{{ route('hub.agents.templates') }}">
    Templates

Evidence - Templates.php templates() computed property (lines 76-93):

#[Computed]
public function templates(): Collection
{
    $templates = $this->templateService->list();

    if ($this->category) {
        $templates = $templates->filter(fn ($t) => $t['category'] === $this->category);
    }

    if ($this->search) {
        $search = strtolower($this->search);
        $templates = $templates->filter(fn ($t) => ...);
    }

    return $templates->values();
}

Evidence - View grid (templates.blade.php lines 60-149):

<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
    @foreach($this->templates as $template)
        <flux:card class="p-6 flex flex-col">
            <!-- Template card with name, category, description, variables -->
        </flux:card>
    @endforeach
</div>

5 templates available: bug-fix, code-review, feature-port, new-feature, refactor

AC26: Template preview shows phases and variables

Method: Code inspection of Templates.php and templates.blade.php

Evidence - previewTemplate() computed property (Templates.php lines 107-115):

#[Computed]
public function previewTemplate(): ?array
{
    if (! $this->previewSlug) {
        return null;
    }
    return $this->templateService->previewTemplate($this->previewSlug, []);
}

Evidence - Preview modal phases (templates.blade.php lines 207-235):

<flux:heading size="sm" class="mb-4">Phases ({{ count($this->previewTemplate['phases']) }})</flux:heading>
<div class="space-y-4">
    @foreach($this->previewTemplate['phases'] as $index => $phase)
        <div class="border rounded-lg p-4">
            <span class="w-6 h-6 rounded-full bg-violet-100">{{ $phase['order'] }}</span>
            <flux:heading size="sm">{{ $phase['name'] }}</flux:heading>
            @if(!empty($phase['tasks']))
                <ul class="ml-9 space-y-1">
                    @foreach($phase['tasks'] as $task)
                        <li>{{ is_array($task) ? $task['name'] : $task }}</li>
                    @endforeach
                </ul>
            @endif
        </div>
    @endforeach
</div>

Evidence - Preview modal variables table (templates.blade.php lines 237-274):

@if(!empty($variables))
    <flux:heading size="sm" class="mb-3">Variables</flux:heading>
    <table class="w-full text-sm">
        <thead>
            <tr><th>Variable</th><th>Description</th><th>Default</th><th>Required</th></tr>
        </thead>
        <tbody>
            @foreach($variables as $name => $config)
                <tr>
                    <td class="font-mono text-violet-600">{{ $name }}</td>
                    <td>{{ $config['description'] ?? '-' }}</td>
                    <td>{{ $config['default'] ?? '-' }}</td>
                    <td>{{ $config['required'] ? 'Yes' : 'No' }}</td>
                </tr>
            @endforeach
        </tbody>
    </table>
@endif

Preview shows: Phases with order/name/tasks, Variables with name/description/default/required, Guidelines

AC27: Can create new plan from template with variable input

Method: Code inspection of Templates.php and templates.blade.php

Evidence - openCreateModal() (Templates.php lines 162-188):

public function openCreateModal(string $slug): void
{
    $template = $this->templateService->get($slug);
    $this->createTemplateSlug = $slug;
    $this->createTitle = $template['name'];
    $this->createWorkspaceId = $this->workspaces->first()?->id ?? 0;

    // Initialise variables with defaults
    $this->createVariables = [];
    foreach ($template['variables'] ?? [] as $name => $config) {
        $this->createVariables[$name] = $config['default'] ?? '';
    }
    $this->showCreateModal = true;
}

Evidence - createPlan() (Templates.php lines 198-272):

public function createPlan(): void
{
    // Validate required variables
    $this->validate([
        'createWorkspaceId' => 'required|exists:workspaces,id',
        'createTitle' => 'required|string|max:255',
    ]);

    // Add variable validation
    foreach ($template['variables'] ?? [] as $name => $config) {
        if ($config['required'] ?? false) {
            $rules["createVariables.{$name}"] = 'required|string';
        }
    }

    // Create the plan via service
    $plan = $this->templateService->createPlan(
        $this->createTemplateSlug,
        $this->createVariables,
        ['title' => $this->createTitle, 'activate' => $this->createActivate],
        $workspace
    );

    // Redirect to new plan
    $this->redirect(route('hub.agents.plans.show', $plan->slug), navigate: true);
}

Evidence - Create modal with variable inputs (templates.blade.php lines 319-341):

@if(!empty($this->createTemplate['variables']))
    <flux:heading size="sm" class="mb-3">Template Variables</flux:heading>
    <div class="space-y-4">
        @foreach($this->createTemplate['variables'] as $name => $config)
            <flux:input
                wire:model="createVariables.{{ $name }}"
                label="{{ ucfirst($name) }}{{ $config['required'] ? ' *' : '' }}"
                placeholder="{{ $config['description'] ?? 'Enter value...' }}"
            />
        @endforeach
    </div>
@endif

Create plan features: Plan title, Workspace selection, Variable inputs, Activate immediately option, Live preview

AC28: Template categories displayed for organisation

Method: Code inspection of Templates.php, PlanTemplateService.php, and view

Evidence - categories() computed property (Templates.php lines 95-99):

#[Computed]
public function categories(): Collection
{
    return $this->templateService->getCategories();
}

Evidence - PlanTemplateService getCategories() (lines 319-326):

public function getCategories(): Collection
{
    return $this->list()
        ->pluck('category')
        ->unique()
        ->sort()
        ->values();
}

Evidence - Category filter dropdown (templates.blade.php lines 45-50):

<flux:select wire:model.live="category" class="w-48">
    <flux:select.option value="">All Categories</flux:select.option>
    @foreach($this->categories as $cat)
        <flux:select.option value="{{ $cat }}">{{ ucfirst($cat) }}</flux:select.option>
    @endforeach
</flux:select>

Evidence - Category badge in template cards (templates.blade.php lines 69-71):

<span class="inline-block px-2 py-0.5 text-xs font-medium rounded-full {{ $this->getCategoryColor($template['category']) }}">
    {{ ucfirst($template['category']) }}
</span>

Evidence - Category colour coding (Templates.php lines 436-446):

public function getCategoryColor(string $category): string
{
    return match ($category) {
        'development' => 'bg-blue-100 text-blue-700 ...',
        'maintenance' => 'bg-green-100 text-green-700 ...',
        'review' => 'bg-amber-100 text-amber-700 ...',
        'migration' => 'bg-purple-100 text-purple-700 ...',
        'custom' => 'bg-zinc-100 text-zinc-700 ...',
        default => 'bg-violet-100 text-violet-700 ...',
    };
}

Category features: Filter dropdown, Colour-coded badges, Stats showing category count

AC29: Can import custom templates (YAML upload)

Method: Code inspection of Templates.php and templates.blade.php

Evidence - Import modal state (Templates.php lines 53-62):

public bool $showImportModal = false;
public ?UploadedFile $importFile = null;
public string $importFileName = '';
public ?array $importPreview = null;
public ?string $importError = null;

Evidence - updatedImportFile() validation (Templates.php lines 293-351):

public function updatedImportFile(): void
{
    $content = file_get_contents($this->importFile->getRealPath());
    $parsed = Yaml::parse($content);

    // Validate basic structure
    if (! is_array($parsed)) {
        $this->importError = 'Invalid YAML format: expected an object.';
        return;
    }
    if (! isset($parsed['name'])) {
        $this->importError = 'Template must have a "name" field.';
        return;
    }
    if (! isset($parsed['phases']) || ! is_array($parsed['phases'])) {
        $this->importError = 'Template must have a "phases" array.';
        return;
    }

    // Build preview
    $this->importPreview = [
        'name' => $parsed['name'],
        'description' => $parsed['description'] ?? null,
        'category' => $parsed['category'] ?? 'custom',
        'phases_count' => count($parsed['phases']),
        'variables_count' => count($parsed['variables'] ?? []),
    ];
}

Evidence - importTemplate() save (Templates.php lines 353-397):

public function importTemplate(): void
{
    $this->validate([
        'importFileName' => 'required|string|regex:/^[a-z0-9-]+$/|max:64',
    ]);

    $content = file_get_contents($this->importFile->getRealPath());
    $targetPath = resource_path("plan-templates/{$this->importFileName}.yaml");

    // Check for existing file
    if (File::exists($targetPath)) {
        $this->importError = 'A template with this filename already exists.';
        return;
    }

    // Save the file
    File::put($targetPath, $content);

    Flux::toast(heading: 'Template Imported', ...);
}

Evidence - Import modal with file upload (templates.blade.php lines 381-476):

<flux:modal wire:model.self="showImportModal" class="max-w-xl">
    <flux:heading size="xl">Import Template</flux:heading>

    {{-- File Upload --}}
    <div class="border-2 border-dashed rounded-lg p-6">
        <input type="file" wire:model="importFile" accept=".yaml,.yml" />
    </div>

    {{-- Preview --}}
    @if($importPreview)
        <dl class="grid grid-cols-2">
            <dt>Name:</dt><dd>{{ $importPreview['name'] }}</dd>
            <dt>Category:</dt><dd>{{ $importPreview['category'] }}</dd>
            <dt>Phases:</dt><dd>{{ $importPreview['phases_count'] }}</dd>
            <dt>Variables:</dt><dd>{{ $importPreview['variables_count'] }}</dd>
        </dl>

        <flux:input wire:model="importFileName" label="Template Filename" />
    @endif

    <flux:button type="submit">Import Template</flux:button>
</flux:modal>

Import features: YAML file upload, Validation (structure, name, phases), Preview before import, Custom filename, Duplicate detection

Phase 5 Summary

AC Description Evidence Status
AC25 Route lists templates routes/web.php:195, templates() computed, grid layout PASS
AC26 Preview shows phases/variables previewTemplate(), modal with phases list + variables table PASS
AC27 Create plan with variables createPlan(), variable inputs in modal, validation PASS
AC28 Categories displayed getCategories(), filter dropdown, colour-coded badges PASS
AC29 YAML import importTemplate(), file upload, validation, preview PASS

Additional Implementation Verified:

  • Hades access control in component (checkHadesAccess())
  • Sidebar link at line 573
  • Dashboard quick link activated
  • Template deletion with confirmation
  • 5 templates available: bug-fix, code-review, feature-port, new-feature, refactor
  • Stats cards: total templates, categories, phases, with variables
  • Search functionality with debounce
  • Category colour coding for visual organisation

Phase 5 Status: VERIFIED — Ready for human approval


Implementation Hints (Added by Planning Agent)

Existing Code Reference

Agent models already exist — don't recreate them:

Model Location Key Methods
AgentPlan app/Models/Agent/AgentPlan.php getProgress(), getCurrentPhase(), activate(), complete(), archive(), toMcpContext()
AgentPhase app/Models/Agent/AgentPhase.php Status constants: STATUS_PENDING, STATUS_IN_PROGRESS, STATUS_COMPLETED, STATUS_BLOCKED, STATUS_SKIPPED
AgentSession app/Models/Agent/AgentSession.php start(), pause(), resume(), complete(), fail(), logAction(), prepareHandoff(), getDurationFormatted()
McpToolCallStat app/Models/Mcp/McpToolCallStat.php getTopTools(), getDailyTrend(), getServerStats() — these are ready-made for dashboards

Hades Auth Pattern

Follow existing pattern from other Hades-only components:

// In mount() method
public function mount(): void
{
    if (!auth()->user()?->isHades()) {
        abort(403);
    }
    // ... rest of mount
}

See examples: PackageManager.php:21, FeatureManager.php:20, Platform.php:36

Sidebar Integration

Add to resources/views/admin/components/sidebar.blade.php after line 376 (before the closing </ul> of the Hades section), using the existing pattern:

{{-- Agent Operations --}}
<li class="mb-0.5" x-data="{ expanded: {{ request()->routeIs('hub.agents.*') ? 'true' : 'false' }} }">
    <a class="block text-gray-800 dark:text-gray-100 truncate transition hover:text-gray-900 dark:hover:text-white pl-4 pr-3 py-2 rounded-lg @if(request()->routeIs('hub.agents.*')) bg-linear-to-r from-violet-500/[0.12] dark:from-violet-500/[0.24] to-violet-500/[0.04] @endif"
       href="{{ route('hub.agents.index') }}"
       @click.prevent="expanded = !expanded">
        <div class="flex items-center justify-between">
            <div class="flex items-center">
                <x-icon name="robot" class="shrink-0 {{ request()->routeIs('hub.agents.*') ? 'text-violet-500' : 'text-violet-400' }}" />
                <span class="text-sm font-medium ml-4 lg:opacity-0 lg:sidebar-expanded:opacity-100 2xl:opacity-100 duration-200">Agents</span>
            </div>
            <div class="flex shrink-0 ml-2 lg:opacity-0 lg:sidebar-expanded:opacity-100 2xl:opacity-100 duration-200">
                <svg class="w-3 h-3 shrink-0 fill-current text-gray-400 dark:text-gray-500 transition-transform duration-200" :class="{ 'rotate-180': expanded }" viewBox="0 0 12 12">
                    <path d="M5.9 11.4L.5 6l1.4-1.4 4 4 4-4L11.3 6z" />
                </svg>
            </div>
        </div>
    </a>
    <div class="lg:hidden lg:sidebar-expanded:block 2xl:block" x-show="expanded" x-cloak>
        <ul class="pl-10 mt-1 space-y-1">
            <li><a class="block text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 truncate transition text-sm py-1 @if(request()->routeIs('hub.agents.index')) !text-violet-500 @endif" href="{{ route('hub.agents.index') }}">Dashboard</a></li>
            <li><a class="block text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 truncate transition text-sm py-1 @if(request()->routeIs('hub.agents.plans*')) !text-violet-500 @endif" href="{{ route('hub.agents.plans') }}">Plans</a></li>
            <li><a class="block text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 truncate transition text-sm py-1 @if(request()->routeIs('hub.agents.sessions*')) !text-violet-500 @endif" href="{{ route('hub.agents.sessions') }}">Sessions</a></li>
            <li><a class="block text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 truncate transition text-sm py-1 @if(request()->routeIs('hub.agents.tools*')) !text-violet-500 @endif" href="{{ route('hub.agents.tools') }}">Tool Analytics</a></li>
            <li><a class="block text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 truncate transition text-sm py-1 @if(request()->routeIs('hub.agents.api-keys')) !text-violet-500 @endif" href="{{ route('hub.agents.api-keys') }}">API Keys</a></li>
            <li><a class="block text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 truncate transition text-sm py-1 @if(request()->routeIs('hub.agents.templates')) !text-violet-500 @endif" href="{{ route('hub.agents.templates') }}">Templates</a></li>
        </ul>
    </div>
</li>

Admin Layout Pattern

Use existing admin layout:

public function render()
{
    return view('admin.admin.agent.dashboard')
        ->layout('admin.layouts.app', ['title' => 'Agent Dashboard']);
}

View Location

Views go in resources/views/admin/livewire/agent/ (matches other admin components)

Dashboard Queries (Ready to Use)

// These methods already exist on McpToolCallStat
$topTools = McpToolCallStat::getTopTools(days: 7, limit: 10);
$dailyTrend = McpToolCallStat::getDailyTrend(days: 7);
$serverStats = McpToolCallStat::getServerStats(days: 7);

// AgentPlan queries
$activePlans = AgentPlan::active()->with(['workspace', 'agentPhases'])->count();
$activeSessions = AgentSession::active()->count();

Routes Location

Add after line 164 in routes/web.php (inside the hub group, after Commerce routes):

// Check existing structure at lines 131-169 for context

This task creates the human interface for supervising AI agent operations.