# 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 ✅
- [x] AC1: Route `hub.agents.plans` shows all AgentPlans (filterable by workspace, status)
- [x] AC2: Plan list shows: title, workspace, status, phase progress, last activity
- [x] AC3: Plan detail view shows all phases with task checklists
- [x] AC4: Phase progress displayed as visual percentage bar
- [x] AC5: Can manually update plan status (activate, complete, archive)
- [x] AC6: Can manually update phase status (start, complete, block, skip)
### Phase 2: Session Monitor ✅
- [x] AC7: Route `hub.agents.sessions` shows all AgentSessions
- [x] AC8: Active sessions show real-time activity (polling or WebSocket)
- [x] AC9: Session detail shows: work log, artifacts, handoff notes
- [x] AC10: Can view session context summary and final summary
- [x] AC11: Timeline view shows session sequence within a plan
- [x] AC12: Can pause/resume/fail active sessions manually
### Phase 3: Tool Analytics ✅
- [x] AC13: Route `hub.agents.tools` shows MCP tool usage dashboard
- [x] AC14: Top 10 tools by usage displayed
- [x] AC15: Daily trend chart (calls per day, 30-day window)
- [x] AC16: Server breakdown shows usage by MCP server
- [x] AC17: Error rate and average duration displayed per tool
- [x] AC18: Can drill down to individual tool calls with full params
### Phase 4: API Key Management ✅
- [x] AC19: Route `hub.agents.api-keys` manages agent API access
- [x] AC20: Can create API keys scoped to workspace
- [x] AC21: Keys have configurable permissions (read plans, write plans, execute tools)
- [x] AC22: Key usage tracking (last used, call count)
- [x] AC23: Can revoke keys immediately
- [x] AC24: Rate limiting configuration per key
### Phase 5: Plan Templates ✅
- [x] AC25: Route `hub.agents.templates` lists available plan templates
- [x] AC26: Template preview shows phases and variables
- [x] AC27: Can create new plan from template with variable input
- [x] AC28: Template categories displayed for organisation
- [x] AC29: Can import custom templates (YAML upload)
---
## Implementation Checklist
### Routes (`routes/web.php`)
Add after line 164 (after Commerce routes), inside the hub group:
```php
// 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');
});
```
- [x] Add route group `hub/agents`
- [x] Route `hub.agents.index` → Dashboard (overview)
- [x] Route `hub.agents.plans` → Plans (list)
- [x] Route `hub.agents.plans.show` → PlanDetail (single plan by slug)
- [x] Route `hub.agents.sessions` → Sessions (list) *(Phase 2)* ✅
- [x] Route `hub.agents.sessions.show` → SessionDetail (single session) *(Phase 2)* ✅
- [x] Route `hub.agents.tools` → ToolAnalytics (MCP stats) *(Phase 3)* ✅
- [x] Route `hub.agents.tools.calls` → ToolCalls (detailed logs) *(Phase 3)* ✅
- [x] Route `hub.agents.api-keys` → ApiKeys (key management) *(Phase 4)* ✅
- [x] 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().
- [x] 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()`
- [x] Create `Plans.php` — Plan list with filters ✅
- Properties: `$status`, `$workspace`, `$search`, `$perPage`
- Query: `AgentPlan::with(['workspace', 'agentPhases'])->filter()->paginate()`
- Methods: `activate()`, `archive()`, `delete()`
- [x] 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()`
- [x] Create `Sessions.php` — Session list ✅
- Properties: `$status`, `$agentType`, `$planSlug`, `$workspace`, `$search`
- Query: `AgentSession::with(['plan', 'workspace'])->filter()->paginate()`
- Methods: `pause()`, `resume()`, `complete()`, `fail()`, `clearFilters()`
- [x] 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)
- [x] 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
- [x] 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)
- [x] 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
- [x] 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/`)
- [x] Create `dashboard.blade.php` ✅
- [x] Create `plans.blade.php` ✅
- [x] Create `plan-detail.blade.php` ✅
- [x] Create `sessions.blade.php` *(Phase 2)* ✅
- [x] Create `session-detail.blade.php` *(Phase 2)* ✅
- [x] Create `tool-analytics.blade.php` *(Phase 3)* ✅
- [x] Create `tool-calls.blade.php` *(Phase 3)* ✅
- [x] Create `api-keys.blade.php` *(Phase 4)* ✅
- [x] 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
- [x] 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()`
- [x] Migration for `agent_api_keys` table *(Phase 4)* ✅
- [x] Add `api_key_id` to `agent_sessions` table (track which key started session) *(Phase 4)* ✅
- [x] Add `api_key_id` to `mcp_tool_calls` table (track which key made call) *(Phase 4)* ✅
### Services
- [x] Create `AgentApiKeyService.php`: *(Phase 4)* ✅
- `create()`, `revoke()`, `validate()`
- `checkPermission()`, `checkPermissions()`, `recordUsage()`
- `isRateLimited()`, `getRateLimitStatus()`
- `authenticate()` — Full auth flow with rate limiting
- `updatePermissions()`, `updateRateLimit()`
- [x] `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
- [x] 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)
```
- [x] Sessions link added to sidebar *(Phase 2)* ✅
- [x] Sessions quick link activated in dashboard *(Phase 2)* ✅
- [x] Add badge for plans needing attention (blocked phases) ✅
- [x] Tool Analytics link added to sidebar *(Phase 3)* ✅
- [x] Tool Analytics quick link activated in dashboard *(Phase 3)* ✅
- [x] API Keys link added to sidebar *(Phase 4)* ✅
- [x] Templates link added to sidebar *(Phase 5)* ✅
- [x] 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
```php
// 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
```bash
# 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):**
```php
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):**
```php
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):**
```blade
```
**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):**
```blade
Work Log
@foreach($this->recentWorkLog as $entry)
@endforeach
```
**Evidence - Artifacts (lines 228-257):**
```blade
Artifacts
@foreach($this->artifacts as $artifact)
@endforeach
```
**Evidence - Handoff Notes (lines 259-296):**
```blade
Handoff Notes
```
**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):**
```php
#[Computed]
public function contextSummary(): ?array
{
return $this->session->context_summary;
}
```
**Evidence - Context Summary view (lines 110-141):**
```blade
@if($this->contextSummary)
Context Summary
@endif
```
**Evidence - Final Summary view (lines 190-200):**
```blade
@if($session->final_summary)
Final Summary
{{ $session->final_summary }}
@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):**
```php
#[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):**
```php
#[Computed]
public function sessionIndex(): int
{
// Returns 1-based index of current session in plan
}
```
**Evidence - Timeline view (lines 71-105):**
```blade
@if($session->agent_plan_id && $this->planSessions->count() > 1)
Plan Timeline (Session {{ $this->sessionIndex }} of {{ $this->planSessions->count() }})
@foreach($this->planSessions as $index => $planSession)
@endforeach
@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):**
```php
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):**
```php
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):**
```blade
@if($session->isActive())
Pause
@endif
@if($session->isPaused())
Resume
@endif
Complete
Fail
```
**Evidence - Session detail buttons (session-detail.blade.php lines 28-39):**
```blade
@if($session->isActive())
Pause
@elseif($session->isPaused())
Resume
@endif
@if(!$session->isEnded())
Complete
Fail
@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):**
```php
/ 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):**
```php
#[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):**
```php
#[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):**
```blade
Top 10 Tools
| Tool | Server | Calls | Success Rate | Errors | Avg Duration |
@foreach($this->topTools as $tool)
@endforeach
```
**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):**
```php
#[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):**
```blade
```
**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):**
```php
#[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):**
```blade
Server Breakdown
@foreach($this->serverStats as $serverStat)
{{ $serverStat->server_id }}
{{ number_format($serverStat->total_calls) }} calls
{{ $serverStat->unique_tools }} tools
{{ $serverStat->success_rate }}% success
@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):**
```blade
Success Rate |
Errors |
Avg Duration |
...
{{ $tool->success_rate }}%
|
@if($tool->total_errors > 0)
{{ number_format($tool->total_errors) }}
@else
0
@endif
|
@if($tool->avg_duration)
{{ round($tool->avg_duration) < 1000 ? round($tool->avg_duration) . 'ms' : round($tool->avg_duration / 1000, 2) . 's' }}
@endif
|
```
**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):**
```php
#[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):**
```blade
@if($selectedCall)
{{ $selectedCall->tool_name }}
{{-- Input Parameters --}}
@if($selectedCall->input_params && count($selectedCall->input_params) > 0)
{{ json_encode($selectedCall->input_params, JSON_PRETTY_PRINT) }}
@endif
{{-- Error Details --}}
@if(!$selectedCall->success)
Code: {{ $selectedCall->error_code }}
{{ $selectedCall->error_message }}
@endif
{{-- Result Summary --}}
@if($selectedCall->result_summary && count($selectedCall->result_summary) > 0)
{{ json_encode($selectedCall->result_summary, JSON_PRETTY_PRINT) }}
@endif
@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):**
```php
Route::get('/api-keys', \App\Livewire\Admin\Agent\ApiKeys::class)->name('api-keys');
```
**Evidence - Sidebar link (sidebar.blade.php line 568):**
```blade
API Keys
```
**Evidence - Hades access control (ApiKeys.php lines 49-55):**
```php
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):**
```php
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):**
```blade
Select workspace...
@foreach($this->workspaces as $ws)
{{ $ws->name }}
@endforeach
```
**Evidence - AgentApiKey model (lines 113-118):**
```php
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):**
```php
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):**
```php
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):**
```blade
@foreach(\Mod\Agentic\Models\AgentApiKey::availablePermissions() as $perm => $description)
@endforeach
```
**Evidence - Permission helpers (AgentApiKey.php lines 195-220):**
```php
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):**
```php
$table->unsignedBigInteger('call_count')->default(0);
$table->timestamp('last_used_at')->nullable();
```
**Evidence - AgentApiKey recordUsage() (lines 230-236):**
```php
public function recordUsage(): self
{
$this->increment('call_count');
$this->update(['last_used_at' => now()]);
return $this;
}
```
**Evidence - AgentApiKeyService recordUsage() (lines 79-91):**
```php
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):**
```blade
{{ number_format($key->call_count) }} |
{{ $key->getLastUsedForHumans() }} |
```
**Evidence - Stats card (api-keys.blade.php lines 54-59):**
```blade
{{ number_format($this->stats['total_calls']) }}
Total Calls
```
**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):**
```php
public function revoke(): self
{
$this->update(['revoked_at' => now()]);
return $this;
}
```
**Evidence - AgentApiKeyService revoke() (lines 129-135):**
```php
public function revoke(AgentApiKey $key): void
{
$key->revoke();
// Clear rate limit cache
Cache::forget($this->getRateLimitCacheKey($key));
}
```
**Evidence - ApiKeys.php revokeKey() (lines 118-128):**
```php
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):**
```blade
Revoke Key
```
**Evidence - isRevoked helper (AgentApiKey.php lines 184-187):**
```php
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):**
```php
$table->integer('rate_limit')->default(100); // Calls per minute
```
**Evidence - AgentApiKeyService rate limiting (lines 96-124):**
```php
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):**
```php
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):**
```blade
```
**Evidence - Table column (api-keys.blade.php line 102):**
```blade
{{ $key->rate_limit }}/min |
```
**Evidence - Edit modal rate limit (api-keys.blade.php lines 334-336):**
```blade
```
**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):**
```php
Route::get('/templates', \App\Livewire\Admin\Agent\Templates::class)->name('templates');
```
**Evidence - Sidebar link (sidebar.blade.php lines 573-574):**
```blade
Templates
```
**Evidence - Templates.php templates() computed property (lines 76-93):**
```php
#[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):**
```blade
@foreach($this->templates as $template)
@endforeach
```
**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):**
```php
#[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):**
```blade
Phases ({{ count($this->previewTemplate['phases']) }})
@foreach($this->previewTemplate['phases'] as $index => $phase)
{{ $phase['order'] }}
{{ $phase['name'] }}
@if(!empty($phase['tasks']))
@foreach($phase['tasks'] as $task)
- {{ is_array($task) ? $task['name'] : $task }}
@endforeach
@endif
@endforeach
```
**Evidence - Preview modal variables table (templates.blade.php lines 237-274):**
```blade
@if(!empty($variables))
Variables
| Variable | Description | Default | Required |
@foreach($variables as $name => $config)
| {{ $name }} |
{{ $config['description'] ?? '-' }} |
{{ $config['default'] ?? '-' }} |
{{ $config['required'] ? 'Yes' : 'No' }} |
@endforeach
@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):**
```php
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):**
```php
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):**
```blade
@if(!empty($this->createTemplate['variables']))
Template Variables
@foreach($this->createTemplate['variables'] as $name => $config)
@endforeach
@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):**
```php
#[Computed]
public function categories(): Collection
{
return $this->templateService->getCategories();
}
```
**Evidence - PlanTemplateService getCategories() (lines 319-326):**
```php
public function getCategories(): Collection
{
return $this->list()
->pluck('category')
->unique()
->sort()
->values();
}
```
**Evidence - Category filter dropdown (templates.blade.php lines 45-50):**
```blade
All Categories
@foreach($this->categories as $cat)
{{ ucfirst($cat) }}
@endforeach
```
**Evidence - Category badge in template cards (templates.blade.php lines 69-71):**
```blade
{{ ucfirst($template['category']) }}
```
**Evidence - Category colour coding (Templates.php lines 436-446):**
```php
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):**
```php
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):**
```php
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):**
```php
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):**
```blade
Import Template
{{-- File Upload --}}
{{-- Preview --}}
@if($importPreview)
- Name:
- {{ $importPreview['name'] }}
- Category:
- {{ $importPreview['category'] }}
- Phases:
- {{ $importPreview['phases_count'] }}
- Variables:
- {{ $importPreview['variables_count'] }}
@endif
Import Template
```
**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:
```php
// 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 `` of the Hades section), using the existing pattern:
```blade
{{-- Agent Operations --}}
```
### Admin Layout Pattern
Use existing admin layout:
```php
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)
```php
// 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):
```php
// Check existing structure at lines 131-169 for context
```
---
*This task creates the human interface for supervising AI agent operations.*