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>
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:
- Plan Dashboard — See all active plans across workspaces
- Phase Tracking — Visual progress through plan phases
- Session Monitor — Watch agent sessions in real-time
- Tool Analytics — MCP tool usage and performance
- API Key Management — Enable external agent access
- 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.plansshows 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.sessionsshows 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.toolsshows 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-keysmanages 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.templateslists 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()
- Query:
-
Create
Plans.php— Plan list with filters ✅- Properties:
$status,$workspace,$search,$perPage - Query:
AgentPlan::with(['workspace', 'agentPhases'])->filter()->paginate() - Methods:
activate(),archive(),delete()
- Properties:
-
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()
- Property:
-
Create
Sessions.php— Session list ✅- Properties:
$status,$agentType,$planSlug,$workspace,$search - Query:
AgentSession::with(['plan', 'workspace'])->filter()->paginate() - Methods:
pause(),resume(),complete(),fail(),clearFilters()
- Properties:
-
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)
- Property:
-
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
- Query:
-
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)
- Properties:
-
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
- Methods:
-
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
- Uses:
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/agentswith API key auth GET /api/v1/agents/plans— List plans for workspacePOST /api/v1/agents/plans— Create plan (from template or raw)GET /api/v1/agents/plans/{slug}— Get plan detailPATCH /api/v1/agents/plans/{slug}— Update plan statusPOST /api/v1/agents/plans/{slug}/phases/{id}/tasks— Add task to phasePATCH /api/v1/agents/plans/{slug}/phases/{id}/tasks/{name}— Complete taskPOST /api/v1/agents/sessions— Start new sessionPATCH /api/v1/agents/sessions/{id}— Update session (log, artifacts, handoff)POST /api/v1/agents/sessions/{id}/complete— End session with summaryGET /api/v1/agents/templates— List available templatesPOST /api/v1/agents/templates/{slug}/instantiate— Create plan from template
Models & Migrations
- Create
AgentApiKeymodel 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_keystable (Phase 4) ✅ - Add
api_key_idtoagent_sessionstable (track which key started session) (Phase 4) ✅ - Add
api_key_idtomcp_tool_callstable (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 limitingupdatePermissions(),updateRateLimit()
PlanTemplateService.phpalready 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 plansagents.sessions.concurrent— Max concurrent sessionsagents.tools.calls— Monthly tool call limitagents.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:
- Claude Code creates plan for Host Hub work
- Cursor picks up session for code changes
- Host Hub AI generates content as directed
- 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 dashboardhub.agents.tools.calls→ ToolCalls drill-down list
Livewire Components Created:
app/Livewire/Admin/Agent/ToolAnalytics.php- Dashboard with stats, charts, filtersapp/Livewire/Admin/Agent/ToolCalls.php- Paginated call list with modal detail
Views Created:
resources/views/admin/livewire/agent/tool-analytics.blade.phpresources/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()
- Key generation with
Migration Created:
database/migrations/2026_01_02_220000_create_agent_api_keys_table.php- Creates
agent_api_keystable with key, permissions (JSON), rate_limit, call_count, timestamps - Adds
agent_api_key_idforeign key toagent_sessionsandmcp_tool_calls
- Creates
Service Created:
app/Services/Agent/AgentApiKeyService.phpcreate()— Generate new key with permissions and rate limitvalidate()— Validate plaintext key and return modelcheckPermission(),checkPermissions()— Verify accessrecordUsage()— Increment call count and cache for rate limitingisRateLimited(),getRateLimitStatus()— Rate limiting helpersauthenticate()— Full auth flow returning structured resultrevoke(),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_sessionsandmcp_tool_callsfor 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
PlanTemplateServicefor 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
- Uses existing
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 workflowcode-review.yaml- Code review processfeature-port.yaml- Feature portingnew-feature.yaml- New feature developmentrefactor.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.