2215 lines
82 KiB
Markdown
2215 lines
82 KiB
Markdown
|
|
# 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
|
||
|
|
<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):**
|
||
|
|
```blade
|
||
|
|
<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):**
|
||
|
|
```blade
|
||
|
|
<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):**
|
||
|
|
```blade
|
||
|
|
@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):**
|
||
|
|
```php
|
||
|
|
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):**
|
||
|
|
```php
|
||
|
|
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):**
|
||
|
|
```php
|
||
|
|
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):**
|
||
|
|
```blade
|
||
|
|
<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):**
|
||
|
|
```php
|
||
|
|
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):**
|
||
|
|
```php
|
||
|
|
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):**
|
||
|
|
```php
|
||
|
|
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):**
|
||
|
|
```blade
|
||
|
|
<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):**
|
||
|
|
```blade
|
||
|
|
<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):**
|
||
|
|
```blade
|
||
|
|
<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):**
|
||
|
|
```blade
|
||
|
|
<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):**
|
||
|
|
```php
|
||
|
|
#[Computed]
|
||
|
|
public function contextSummary(): ?array
|
||
|
|
{
|
||
|
|
return $this->session->context_summary;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Evidence - Context Summary view (lines 110-141):**
|
||
|
|
```blade
|
||
|
|
@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):**
|
||
|
|
```blade
|
||
|
|
@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):**
|
||
|
|
```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)
|
||
|
|
<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):**
|
||
|
|
```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
|
||
|
|
<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):**
|
||
|
|
```blade
|
||
|
|
@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):**
|
||
|
|
```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
|
||
|
|
<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):**
|
||
|
|
```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
|
||
|
|
<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):**
|
||
|
|
```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
|
||
|
|
<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):**
|
||
|
|
```blade
|
||
|
|
<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):**
|
||
|
|
```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)
|
||
|
|
<flux:modal wire:model.self="selectedCallId" class="max-w-4xl">
|
||
|
|
<flux:heading size="lg">{{ $selectedCall->tool_name }}</flux:heading>
|
||
|
|
<!-- Metadata: Duration, Agent Type, Workspace, Time -->
|
||
|
|
<!-- Session ID -->
|
||
|
|
<!-- Plan link -->
|
||
|
|
|
||
|
|
{{-- Input Parameters --}}
|
||
|
|
@if($selectedCall->input_params && count($selectedCall->input_params) > 0)
|
||
|
|
<div class="bg-zinc-100 rounded-lg p-4">
|
||
|
|
<pre>{{ json_encode($selectedCall->input_params, JSON_PRETTY_PRINT) }}</pre>
|
||
|
|
</div>
|
||
|
|
@endif
|
||
|
|
|
||
|
|
{{-- Error Details --}}
|
||
|
|
@if(!$selectedCall->success)
|
||
|
|
<div class="p-4 bg-red-50 border border-red-200 rounded-lg">
|
||
|
|
<flux:text>Code: {{ $selectedCall->error_code }}</flux:text>
|
||
|
|
<flux:text>{{ $selectedCall->error_message }}</flux:text>
|
||
|
|
</div>
|
||
|
|
@endif
|
||
|
|
|
||
|
|
{{-- Result Summary --}}
|
||
|
|
@if($selectedCall->result_summary && count($selectedCall->result_summary) > 0)
|
||
|
|
<pre>{{ json_encode($selectedCall->result_summary, JSON_PRETTY_PRINT) }}</pre>
|
||
|
|
@endif
|
||
|
|
</flux:modal>
|
||
|
|
@endif
|
||
|
|
```
|
||
|
|
|
||
|
|
**Drill-down features:**
|
||
|
|
- Modal with full tool call details
|
||
|
|
- Input parameters (JSON pretty-printed)
|
||
|
|
- Result summary (JSON pretty-printed)
|
||
|
|
- Error details (code + message) for failed calls
|
||
|
|
- Metadata: duration, agent type, workspace, timestamp
|
||
|
|
- Session and Plan links for traceability
|
||
|
|
- Filters: server, tool, status, workspace, agent type
|
||
|
|
- Pagination with 25 per page ✅
|
||
|
|
|
||
|
|
### Phase 3 Summary
|
||
|
|
|
||
|
|
| AC | Description | Evidence | Status |
|
||
|
|
|----|-------------|----------|--------|
|
||
|
|
| AC13 | Tool usage dashboard route | `routes/web.php:190`, `ToolAnalytics.php` with Hades auth | ✅ PASS |
|
||
|
|
| AC14 | Top 10 tools by usage | `topTools()` computed, table with 6 columns | ✅ PASS |
|
||
|
|
| AC15 | Daily trend chart | Chart.js line chart, configurable 7-90 days | ✅ PASS |
|
||
|
|
| AC16 | Server breakdown | `serverStats()` computed, progress bars, success rates | ✅ PASS |
|
||
|
|
| AC17 | Error rate and duration | Table columns + colour-coded success rate | ✅ PASS |
|
||
|
|
| AC18 | Drill-down to tool calls | Modal with input_params, result_summary, error_details | ✅ PASS |
|
||
|
|
|
||
|
|
**Additional Implementation Verified:**
|
||
|
|
- Hades access control in both components
|
||
|
|
- Sidebar link at `hub.agents.tools`
|
||
|
|
- Dashboard quick link to Tool Analytics
|
||
|
|
- Drill-down links from Top Tools table
|
||
|
|
- Recent Errors section in dashboard
|
||
|
|
- Comprehensive filtering in ToolCalls
|
||
|
|
- Pagination with proper links
|
||
|
|
|
||
|
|
**Phase 3 Status:** ✅ VERIFIED — Ready for human approval
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Phase 3 Implementation Notes (2026-01-02 21:30)
|
||
|
|
|
||
|
|
**Implementation Agent:** Claude Opus 4.5
|
||
|
|
|
||
|
|
**Routes Added (`routes/web.php`):**
|
||
|
|
- `hub.agents.tools` → ToolAnalytics dashboard
|
||
|
|
- `hub.agents.tools.calls` → ToolCalls drill-down list
|
||
|
|
|
||
|
|
**Livewire Components Created:**
|
||
|
|
- `app/Livewire/Admin/Agent/ToolAnalytics.php` - Dashboard with stats, charts, filters
|
||
|
|
- `app/Livewire/Admin/Agent/ToolCalls.php` - Paginated call list with modal detail
|
||
|
|
|
||
|
|
**Views Created:**
|
||
|
|
- `resources/views/admin/livewire/agent/tool-analytics.blade.php`
|
||
|
|
- `resources/views/admin/livewire/agent/tool-calls.blade.php`
|
||
|
|
|
||
|
|
**Phase 5:** Not yet implemented (commented out in routes)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Phase 4 Implementation Notes (2026-01-02 22:30)
|
||
|
|
|
||
|
|
**Implementation Agent:** Claude Opus 4.5
|
||
|
|
|
||
|
|
**Route Added (`routes/web.php`):**
|
||
|
|
- `hub.agents.api-keys` → ApiKeys management
|
||
|
|
|
||
|
|
**Model Created:**
|
||
|
|
- `app/Models/Agent/AgentApiKey.php`
|
||
|
|
- Key generation with `ak_` prefix + 32 random chars
|
||
|
|
- SHA-256 hashing for storage
|
||
|
|
- Permission constants: `PERM_PLANS_READ`, `PERM_PLANS_WRITE`, `PERM_PHASES_WRITE`, `PERM_SESSIONS_READ`, `PERM_SESSIONS_WRITE`, `PERM_TOOLS_READ`, `PERM_TEMPLATES_READ`, `PERM_TEMPLATES_INSTANTIATE`
|
||
|
|
- Status helpers: `isActive()`, `isRevoked()`, `isExpired()`
|
||
|
|
- Scopes: `active()`, `revoked()`, `expired()`, `forWorkspace()`
|
||
|
|
|
||
|
|
**Migration Created:**
|
||
|
|
- `database/migrations/2026_01_02_220000_create_agent_api_keys_table.php`
|
||
|
|
- Creates `agent_api_keys` table with key, permissions (JSON), rate_limit, call_count, timestamps
|
||
|
|
- Adds `agent_api_key_id` foreign key to `agent_sessions` and `mcp_tool_calls`
|
||
|
|
|
||
|
|
**Service Created:**
|
||
|
|
- `app/Services/Agent/AgentApiKeyService.php`
|
||
|
|
- `create()` — Generate new key with permissions and rate limit
|
||
|
|
- `validate()` — Validate plaintext key and return model
|
||
|
|
- `checkPermission()`, `checkPermissions()` — Verify access
|
||
|
|
- `recordUsage()` — Increment call count and cache for rate limiting
|
||
|
|
- `isRateLimited()`, `getRateLimitStatus()` — Rate limiting helpers
|
||
|
|
- `authenticate()` — Full auth flow returning structured result
|
||
|
|
- `revoke()`, `updatePermissions()`, `updateRateLimit()`
|
||
|
|
|
||
|
|
**Livewire Component Created:**
|
||
|
|
- `app/Livewire/Admin/Agent/ApiKeys.php`
|
||
|
|
- Stats: total, active, revoked, total calls
|
||
|
|
- Filters: workspace, status
|
||
|
|
- Create modal: name, workspace, permissions, rate limit, expiry
|
||
|
|
- Created key modal: shows plaintext key ONCE
|
||
|
|
- Edit modal: update permissions and rate limit
|
||
|
|
- Revoke action with immediate effect
|
||
|
|
|
||
|
|
**View Created:**
|
||
|
|
- `resources/views/admin/livewire/agent/api-keys.blade.php`
|
||
|
|
|
||
|
|
**Sidebar Updated:**
|
||
|
|
- Added API Keys link to Agents submenu
|
||
|
|
|
||
|
|
**Phase 4 Status:** ✅ COMPLETE — Ready for verification
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Phase 4 Verification (2026-01-02 23:15)
|
||
|
|
|
||
|
|
**Verification Agent:** Claude Opus 4.5
|
||
|
|
**Verdict:** ✅ PASS - All Phase 4 acceptance criteria met
|
||
|
|
|
||
|
|
#### AC19: Route `hub.agents.api-keys` manages agent API access ✅
|
||
|
|
|
||
|
|
**Method:** Code inspection of `routes/web.php` and `app/Livewire/Admin/Agent/ApiKeys.php`
|
||
|
|
|
||
|
|
**Evidence - Route (routes/web.php line 193):**
|
||
|
|
```php
|
||
|
|
Route::get('/api-keys', \App\Livewire\Admin\Agent\ApiKeys::class)->name('api-keys');
|
||
|
|
```
|
||
|
|
|
||
|
|
**Evidence - Sidebar link (sidebar.blade.php line 568):**
|
||
|
|
```blade
|
||
|
|
<a class="..." href="{{ route('hub.agents.api-keys') }}">API Keys</a>
|
||
|
|
```
|
||
|
|
|
||
|
|
**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
|
||
|
|
<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):**
|
||
|
|
```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
|
||
|
|
<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):**
|
||
|
|
```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
|
||
|
|
<td>{{ number_format($key->call_count) }}</td>
|
||
|
|
<td>{{ $key->getLastUsedForHumans() }}</td>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Evidence - Stats card (api-keys.blade.php lines 54-59):**
|
||
|
|
```blade
|
||
|
|
<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):**
|
||
|
|
```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
|
||
|
|
<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):**
|
||
|
|
```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
|
||
|
|
<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):**
|
||
|
|
```blade
|
||
|
|
<td>{{ $key->rate_limit }}/min</td>
|
||
|
|
```
|
||
|
|
|
||
|
|
**Evidence - Edit modal rate limit (api-keys.blade.php lines 334-336):**
|
||
|
|
```blade
|
||
|
|
<flux:input type="number" wire:model="editKeyRateLimit" label="Rate Limit (calls/min)" min="1" max="10000" />
|
||
|
|
```
|
||
|
|
|
||
|
|
**Rate limit configurable 1-10000 calls/min, enforced via Cache with 60s window** ✅
|
||
|
|
|
||
|
|
### Phase 4 Summary
|
||
|
|
|
||
|
|
| AC | Description | Evidence | Status |
|
||
|
|
|----|-------------|----------|--------|
|
||
|
|
| AC19 | Route manages API access | `routes/web.php:193`, Hades auth, sidebar link | ✅ PASS |
|
||
|
|
| AC20 | Keys scoped to workspace | `workspace_id` FK, workspace dropdown in create modal | ✅ PASS |
|
||
|
|
| AC21 | Configurable permissions | 8 permissions with checkboxes, `hasPermission()` helpers | ✅ PASS |
|
||
|
|
| AC22 | Usage tracking | `call_count` + `last_used_at` fields, displayed in UI | ✅ PASS |
|
||
|
|
| AC23 | Immediate revocation | `revoke()` sets timestamp, clears cache, confirmation dialog | ✅ PASS |
|
||
|
|
| AC24 | Rate limiting per key | Configurable 1-10000/min, Cache-based enforcement | ✅ PASS |
|
||
|
|
|
||
|
|
**Additional Implementation Verified:**
|
||
|
|
- Hades access control in component
|
||
|
|
- Sidebar link at line 568
|
||
|
|
- Stats cards: total, active, revoked, total calls
|
||
|
|
- Create modal shows plaintext key ONCE
|
||
|
|
- Edit modal for permissions and rate limit updates
|
||
|
|
- Key expiry support (optional)
|
||
|
|
- Filter by workspace and status (active/revoked/expired)
|
||
|
|
- Foreign keys added to `agent_sessions` and `mcp_tool_calls` for tracking
|
||
|
|
|
||
|
|
**Phase 4 Status:** ✅ VERIFIED — Ready for human approval
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Phase 5 Implementation Notes (2026-01-02 23:45)
|
||
|
|
|
||
|
|
**Implementation Agent:** Claude Opus 4.5
|
||
|
|
|
||
|
|
**Route Added (`routes/web.php`):**
|
||
|
|
- `hub.agents.templates` → Templates browser
|
||
|
|
|
||
|
|
**Livewire Component Created:**
|
||
|
|
- `app/Livewire/Admin/Agent/Templates.php`
|
||
|
|
- Uses existing `PlanTemplateService` for all template operations
|
||
|
|
- Template listing with category filter and search
|
||
|
|
- Preview modal showing phases, tasks, variables, guidelines
|
||
|
|
- Create plan modal with variable inputs and workspace selection
|
||
|
|
- YAML import modal with file upload, validation, and preview
|
||
|
|
- Delete template functionality
|
||
|
|
|
||
|
|
**View Created:**
|
||
|
|
- `resources/views/admin/livewire/agent/templates.blade.php`
|
||
|
|
- Stats cards: total templates, categories, total phases, with variables
|
||
|
|
- Grid layout for template cards
|
||
|
|
- Category colour coding
|
||
|
|
- Preview modal with full template details
|
||
|
|
- Create modal with variable input fields
|
||
|
|
- Import modal with drag-drop file upload
|
||
|
|
|
||
|
|
**Sidebar Updated:**
|
||
|
|
- Added Templates link to Agents submenu
|
||
|
|
|
||
|
|
**Dashboard Updated:**
|
||
|
|
- Templates quick link now active (was "Coming soon")
|
||
|
|
|
||
|
|
**Existing Templates (5):**
|
||
|
|
- `bug-fix.yaml` - Bug fix workflow
|
||
|
|
- `code-review.yaml` - Code review process
|
||
|
|
- `feature-port.yaml` - Feature porting
|
||
|
|
- `new-feature.yaml` - New feature development
|
||
|
|
- `refactor.yaml` - Code refactoring
|
||
|
|
|
||
|
|
**Phase 5 Status:** ✅ COMPLETE — Ready for verification
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Phase 5 Verification (2026-01-02 23:45)
|
||
|
|
|
||
|
|
**Verification Agent:** Claude Opus 4.5
|
||
|
|
**Verdict:** ✅ PASS - All Phase 5 acceptance criteria met
|
||
|
|
|
||
|
|
#### AC25: Route `hub.agents.templates` lists available plan templates ✅
|
||
|
|
|
||
|
|
**Method:** Code inspection of `routes/web.php` and `app/Livewire/Admin/Agent/Templates.php`
|
||
|
|
|
||
|
|
**Evidence - Route (routes/web.php line 195):**
|
||
|
|
```php
|
||
|
|
Route::get('/templates', \App\Livewire\Admin\Agent\Templates::class)->name('templates');
|
||
|
|
```
|
||
|
|
|
||
|
|
**Evidence - Sidebar link (sidebar.blade.php lines 573-574):**
|
||
|
|
```blade
|
||
|
|
<a class="..." href="{{ route('hub.agents.templates') }}">
|
||
|
|
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
|
||
|
|
<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):**
|
||
|
|
```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
|
||
|
|
<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):**
|
||
|
|
```blade
|
||
|
|
@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):**
|
||
|
|
```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']))
|
||
|
|
<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):**
|
||
|
|
```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
|
||
|
|
<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):**
|
||
|
|
```blade
|
||
|
|
<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):**
|
||
|
|
```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
|
||
|
|
<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:
|
||
|
|
|
||
|
|
```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 `</ul>` of the Hades section), using the existing pattern:
|
||
|
|
|
||
|
|
```blade
|
||
|
|
{{-- 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:
|
||
|
|
|
||
|
|
```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.*
|