Compare commits

...

12 Commits

Author SHA1 Message Date
673ca32d5a Merge pull request 'docs(ms22): add Phase 2 PRD and TASKS for Named Agent Fleet' (#673) from docs/ms22-p2-agent-fleet-prd into main 2026-03-04 20:18:38 +00:00
a777f1f695 docs(ms22): add Phase 2 PRD and TASKS for Named Agent Fleet 2026-03-04 14:17:57 -06:00
d7d8c3c88d Merge pull request 'fix(chat): restrict to authenticated users only, fix overlay transparency' (#672) from fix/chat-auth-only into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-03-04 20:15:13 +00:00
aec8085f60 chore: mark orchestrator session as completed 2026-03-04 14:12:58 -06:00
44da50d0b3 fix(chat): restrict to authenticated users only, fix overlay transparency
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-03-04 11:33:32 -06:00
44fb402ef2 Merge pull request 'ci: use Portainer API for Docker Swarm deploy' (#671) from ci/portainer-v2 into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-03-03 19:01:31 +00:00
f42c47e314 ci: use Portainer API for Docker Swarm deploy
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
2026-03-03 13:00:59 -06:00
8069aeadb5 Merge pull request 'fix(chat): ConfigModule import + CSRF skip for guest endpoint' (#670) from fix/chat-complete into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-03-03 19:00:06 +00:00
1f883c4c04 chore: remove stray file 2026-03-03 12:58:00 -06:00
5207d8c0c9 fix(chat): skip CSRF for guest endpoint
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
2026-03-03 12:36:01 -06:00
d1c9a747b9 fix(chat): import ConfigModule in ChatProxyModule
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
2026-03-03 12:28:50 -06:00
3d669713d7 Merge pull request 'feat(chat): add guest chat mode for unauthenticated users' (#667) from feature/chat-guest-mode into main
Some checks failed
ci/woodpecker/push/ci Pipeline failed
2026-03-03 17:52:08 +00:00
10 changed files with 280 additions and 181 deletions

View File

@@ -80,8 +80,8 @@
"session_id": "sess-002",
"runtime": "unknown",
"started_at": "2026-02-28T20:30:13Z",
"ended_at": "",
"ended_reason": "",
"ended_at": "2026-03-04T13:45:06Z",
"ended_reason": "completed",
"milestone_at_end": "",
"tasks_completed": [],
"last_task_id": ""

View File

@@ -1,8 +0,0 @@
{
"session_id": "sess-002",
"runtime": "unknown",
"pid": 3178395,
"started_at": "2026-02-28T20:30:13Z",
"project_path": "/tmp/ms21-ui-001",
"milestone_id": ""
}

View File

@@ -338,41 +338,43 @@ steps:
- security-trivy-orchestrator
- security-trivy-web
# ─── Deploy to Docker Swarm (main only) ─────────────────────
# ─── Deploy to Docker Swarm via Portainer (main only) ─────────────────────
# ─── Deploy to Docker Swarm via Portainer API (main only) ─────────────────────
deploy-swarm:
image: alpine:3
environment:
SSH_PRIVATE_KEY:
from_secret: ssh_private_key
SSH_KNOWN_HOSTS:
from_secret: ssh_known_hosts
PORTAINER_URL:
from_secret: portainer_url
PORTAINER_API_KEY:
from_secret: portainer_api_key
PORTAINER_STACK_ID: "121"
commands:
- apk add --no-cache curl openssh-client
- apk add --no-cache curl
- |
set -e
echo "🚀 Deploying to Docker Swarm..."
echo "🚀 Deploying to Docker Swarm via Portainer API..."
# Setup SSH for fallback
mkdir -p ~/.ssh
echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
# Use Portainer API to update the stack (forces pull of new images)
RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \
-H "X-API-Key: $PORTAINER_API_KEY" \
-H "Content-Type: application/json" \
"$PORTAINER_URL/api/stacks/$PORTAINER_STACK_ID/git/redeploy")
# Force service updates (images are pulled from public registry)
ssh -o StrictHostKeyChecking=no localadmin@10.1.1.45 \
"docker service update --with-registry-auth --force mosaic-stack-api && \
docker service update --with-registry-auth --force mosaic-stack-web && \
docker service update --with-registry-auth --force mosaic-stack-orchestrator && \
docker service update --with-registry-auth --force mosaic-stack-coordinator && \
echo '✅ All services updated'"
HTTP_CODE=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | head -n -1)
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "202" ]; then
echo "✅ Stack update triggered successfully"
else
echo "❌ Stack update failed (HTTP $HTTP_CODE)"
echo "$BODY"
exit 1
fi
# Wait for services to converge
echo "⏳ Waiting for services to converge..."
sleep 30
echo "✅ Deploy complete"
when:
- branch: [main]
event: [push, manual, tag]

View File

@@ -1,6 +1,7 @@
import { Body, Controller, HttpException, Logger, Post, Req, Res, UseGuards } from "@nestjs/common";
import type { Response } from "express";
import { AuthGuard } from "../auth/guards/auth.guard";
import { SkipCsrf } from "../common/decorators/skip-csrf.decorator";
import type { MaybeAuthenticatedRequest } from "../auth/types/better-auth-request.interface";
import { ChatStreamDto } from "./chat-proxy.dto";
import { ChatProxyService } from "./chat-proxy.service";
@@ -14,6 +15,7 @@ export class ChatProxyController {
// POST /api/chat/guest
// Guest chat endpoint - no authentication required
// Uses a shared LLM configuration for unauthenticated users
@SkipCsrf()
@Post("guest")
async guestChat(
@Body() body: ChatStreamDto,

View File

@@ -1,4 +1,5 @@
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { AuthModule } from "../auth/auth.module";
import { AgentConfigModule } from "../agent-config/agent-config.module";
import { ContainerLifecycleModule } from "../container-lifecycle/container-lifecycle.module";
@@ -7,7 +8,7 @@ import { ChatProxyController } from "./chat-proxy.controller";
import { ChatProxyService } from "./chat-proxy.service";
@Module({
imports: [AuthModule, PrismaModule, ContainerLifecycleModule, AgentConfigModule],
imports: [AuthModule, PrismaModule, ContainerLifecycleModule, AgentConfigModule, ConfigModule],
controllers: [ChatProxyController],
providers: [ChatProxyService],
exports: [ChatProxyService],

View File

@@ -342,6 +342,31 @@ export const Chat = forwardRef<ChatRef, ChatProps>(function Chat(
)}
{/* Input Area */}
{!user && (
<div className="mx-4 mb-2 lg:mx-auto lg:max-w-4xl lg:px-8">
<div
className="flex items-center justify-center gap-2 rounded-lg border px-4 py-3 text-center"
style={{
backgroundColor: "rgb(var(--surface-1))",
borderColor: "rgb(var(--border-default))",
}}
>
<svg
className="h-4 w-4"
style={{ color: "rgb(var(--text-secondary))" }}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
</svg>
<span className="text-sm" style={{ color: "rgb(var(--text-secondary))" }}>
Sign in to chat with Jarvis
</span>
</div>
</div>
)}
<div
className="sticky bottom-0 border-t"
style={{
@@ -352,7 +377,7 @@ export const Chat = forwardRef<ChatRef, ChatProps>(function Chat(
<div className="mx-auto max-w-4xl px-4 py-4 lg:px-8">
<ChatInput
onSend={handleSendMessage}
disabled={isChatLoading}
disabled={isChatLoading || !user}
inputRef={inputRef}
isStreaming={isStreaming}
onStopStreaming={abortStream}

View File

@@ -55,8 +55,8 @@ export function ChatOverlay(): React.JSX.Element {
onClick={open}
className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full shadow-lg transition-all hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2 lg:bottom-8 lg:right-8"
style={{
backgroundColor: "rgb(var(--accent-primary))",
color: "rgb(var(--text-on-accent))",
backgroundColor: "var(--accent-primary, #10b981)",
color: "var(--text-on-accent, #ffffff)",
}}
aria-label="Open chat"
title="Open Jarvis chat (Cmd+Shift+J)"
@@ -78,18 +78,18 @@ export function ChatOverlay(): React.JSX.Element {
if (isMinimized) {
return (
<div
className="fixed bottom-0 right-0 z-40 w-full sm:w-96"
className="fixed bottom-0 right-0 z-40 w-full shadow-2xl sm:w-96"
style={{
backgroundColor: "rgb(var(--surface-0))",
borderColor: "rgb(var(--border-default))",
backgroundColor: "var(--surface-0, #ffffff)",
borderColor: "var(--border-default, #e5e7eb)",
}}
>
<button
onClick={expand}
className="flex w-full items-center justify-between border-t px-4 py-3 text-left transition-colors hover:bg-black/5 focus:outline-none focus:ring-2 focus:ring-inset"
style={{
borderColor: "rgb(var(--border-default))",
backgroundColor: "rgb(var(--surface-0))",
borderColor: "var(--border-default, #e5e7eb)",
backgroundColor: "var(--surface-0, #ffffff)",
}}
aria-label="Expand chat"
>
@@ -135,10 +135,10 @@ export function ChatOverlay(): React.JSX.Element {
{/* Chat Panel */}
<div
className="fixed inset-y-0 right-0 z-40 flex w-full flex-col border-l sm:w-96 lg:inset-y-16"
className="fixed inset-y-0 right-0 z-40 flex w-full flex-col border-l shadow-2xl sm:w-96 lg:inset-y-16"
style={{
backgroundColor: "rgb(var(--surface-0))",
borderColor: "rgb(var(--border-default))",
backgroundColor: "var(--surface-0, #ffffff)",
borderColor: "var(--border-default, #e5e7eb)",
}}
>
{/* Header */}

View File

@@ -4,12 +4,7 @@
*/
import { useState, useCallback, useRef } from "react";
import {
sendChatMessage,
streamChatMessage,
streamGuestChat,
type ChatMessage as ApiChatMessage,
} from "@/lib/api/chat";
import { streamChatMessage, type ChatMessage as ApiChatMessage } from "@/lib/api/chat";
import { createConversation, updateConversation, getIdea, type Idea } from "@/lib/api/ideas";
import { safeJsonParse, isMessageArray } from "@/lib/utils/safe-json";
@@ -219,8 +214,6 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
const controller = new AbortController();
abortControllerRef.current = controller;
let streamingSucceeded = false;
try {
await new Promise<void>((resolve, reject) => {
let hasReceivedData = false;
@@ -248,7 +241,6 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
});
},
() => {
streamingSucceeded = true;
setIsStreaming(false);
abortControllerRef.current = null;
resolve();
@@ -279,70 +271,8 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
return;
}
// Streaming failed - check if auth error, try guest mode
const isAuthError =
err instanceof Error &&
(err.message.includes("403") ||
err.message.includes("401") ||
err.message.includes("auth") ||
err.message.includes("Forbidden"));
if (isAuthError) {
console.warn("Auth failed, trying guest chat mode");
// Try guest chat streaming
try {
await new Promise<void>((guestResolve, guestReject) => {
let hasReceivedData = false;
streamGuestChat(
request,
(chunk: string) => {
if (!hasReceivedData) {
hasReceivedData = true;
setIsLoading(false);
setIsStreaming(true);
setMessages((prev) => {
const updated = [...prev, { ...placeholderMessage }];
messagesRef.current = updated;
return updated;
});
}
setMessages((prev) => {
const updated = prev.map((msg) =>
msg.id === assistantMessageId ? { ...msg, content: msg.content + chunk } : msg
);
messagesRef.current = updated;
return updated;
});
},
() => {
streamingSucceeded = true;
setIsStreaming(false);
guestResolve();
},
(guestErr: Error) => {
guestReject(guestErr);
},
controller.signal
);
});
} catch (guestErr: unknown) {
// Guest also failed
setMessages((prev) => {
const withoutPlaceholder = prev.filter((m) => m.id !== assistantMessageId);
messagesRef.current = withoutPlaceholder;
return withoutPlaceholder;
});
const errorMsg = guestErr instanceof Error ? guestErr.message : "Chat unavailable";
setError(`Unable to connect to chat: ${errorMsg}`);
setIsLoading(false);
return;
}
} else {
// Streaming failed — fall back to non-streaming
console.warn("Streaming failed, falling back to non-streaming", {
// Streaming failed — show error (no guest fallback, auth required)
console.warn("Streaming failed", {
error: err instanceof Error ? err : new Error(String(err)),
});
@@ -352,67 +282,15 @@ export function useChat(options: UseChatOptions = {}): UseChatReturn {
return withoutPlaceholder;
});
setIsStreaming(false);
try {
const response = await sendChatMessage(request);
const assistantMessage: Message = {
id: `assistant-${Date.now().toString()}`,
role: "assistant",
content: response.message.content,
createdAt: new Date().toISOString(),
model: response.model,
promptTokens: response.promptEvalCount ?? 0,
completionTokens: response.evalCount ?? 0,
totalTokens: (response.promptEvalCount ?? 0) + (response.evalCount ?? 0),
};
setMessages((prev) => {
const updated = [...prev, assistantMessage];
messagesRef.current = updated;
return updated;
});
streamingSucceeded = true;
} catch (fallbackErr: unknown) {
const errorMsg =
fallbackErr instanceof Error ? fallbackErr.message : "Failed to send message";
setError("Unable to send message. Please try again.");
onError?.(fallbackErr instanceof Error ? fallbackErr : new Error(errorMsg));
console.error("Failed to send chat message", {
error: fallbackErr,
errorType: "LLM_ERROR",
conversationId: conversationIdRef.current,
messageLength: content.length,
messagePreview: content.substring(0, 50),
model,
messageCount: messagesRef.current.length,
timestamp: new Date().toISOString(),
});
const errorMessage: Message = {
id: `error-${String(Date.now())}`,
role: "assistant",
content: "Something went wrong. Please try again.",
createdAt: new Date().toISOString(),
};
setMessages((prev) => {
const updated = [...prev, errorMessage];
messagesRef.current = updated;
return updated;
});
setIsLoading(false);
const errorMsg = err instanceof Error ? err.message : "Chat unavailable";
setError(`Chat error: ${errorMsg}`);
return;
}
}
}
setIsLoading(false);
if (!streamingSucceeded) {
return;
}
const finalMessages = messagesRef.current;
const isFirstMessage =

View File

@@ -0,0 +1,182 @@
# PRD: MS22 Phase 2 — Named Agent Fleet
## Metadata
- **Owner:** Jason Woltje
- **Date:** 2026-03-04
- **Status:** draft
- **Design Doc:** `~/src/jarvis-brain/docs/planning/FLEET-EVOLUTION-PLAN.md`
- **Depends On:** MS22 Phase 1 (DB-Centric Architecture) — COMPLETE
## Problem Statement
Mosaic Stack has the infrastructure for per-user containers and knowledge layer, but no predefined agent personalities. Users start with a blank slate. For Jason's personal use case, we need named agents with distinct roles, personalities, and tool access that can collaborate through the shared knowledge layer.
## Objectives
1. **Named agents** — jarvis (orchestrator), builder (coding), medic (monitoring)
2. **Per-agent model assignment** — Opus for jarvis, Codex for builder, Haiku for medic
3. **Tool permissions** — Restrict dangerous tools to appropriate agents
4. **Discord bindings** — Route agents to specific channels
5. **Mosaic skill** — All agents can read/write findings and memory
## Scope
### In Scope
- Agent personality definitions (SOUL.md for each)
- Agent registry in Mosaic DB
- Per-agent model configuration
- Per-agent tool permission sets
- Discord channel routing
- Default agent templates for new users
### Out of Scope
- Matrix observation rooms (nice-to-have)
- WebUI chat improvements (separate phase)
- Cross-agent quality gates (future)
- Team workspaces (future)
## Agent Definitions
### Jarvis — Orchestrator
| Property | Value |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| **Role** | Main orchestrator, user-facing assistant |
| **Model** | Opus (primary), Sonnet (fallback) |
| **Tools** | All tools — full access |
| **Discord** | #jarvis |
| **Personality** | Capable, direct, proactive. Gets stuff done without hand-holding. Thinks before acting, speaks up when seeing a better way. NOT a yes-man. |
### Builder — Coding Agent
| Property | Value |
| --------------- | --------------------------------------------------------------------------------------- |
| **Role** | Code implementation, PRs, refactoring |
| **Model** | Codex (primary, uses OpenAI credits), Sonnet (fallback) |
| **Tools** | exec, read, write, edit, github, browser |
| **Discord** | #builder |
| **Personality** | Focused, thorough. Writes clean code. Tests before declaring done. Documents decisions. |
### Medic — Health Monitoring
| Property | Value |
| --------------- | ------------------------------------------------------------------------------- |
| **Role** | System health checks, alerts, monitoring |
| **Model** | Haiku (primary), MiniMax (fallback) |
| **Tools** | exec (SSH), nodes, cron, message (alerts only) |
| **Discord** | #medic-alerts |
| **Personality** | Vigilant, concise. Alerts on anomalies. Proactive health checks. Minimal noise. |
## Database Schema
```prisma
model AgentTemplate {
id String @id @default(cuid())
name String @unique // "jarvis", "builder", "medic"
displayName String // "Jarvis", "Builder", "Medic"
role String // "orchestrator" | "coding" | "monitoring"
personality String // SOUL.md content
primaryModel String // "opus", "codex", "haiku"
fallbackModels Json @default("[]") // ["sonnet", "haiku"]
toolPermissions Json @default("[]") // ["exec", "read", "write", ...]
discordChannel String? // "jarvis", "builder", "medic-alerts"
isActive Boolean @default(true)
isDefault Boolean @default(false) // Include in new user provisioning
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model UserAgent {
id String @id @default(cuid())
userId String
templateId String? // null = custom agent
name String // "jarvis", "builder", "medic" or custom
displayName String
role String
personality String // User can customize
primaryModel String?
fallbackModels Json @default("[]")
toolPermissions Json @default("[]")
discordChannel String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, name])
}
```
## API Endpoints
### Agent Templates (Admin)
```
GET /api/admin/agent-templates — List all templates
POST /api/admin/agent-templates — Create template
GET /api/admin/agent-templates/:id — Get template
PATCH /api/admin/agent-templates/:id — Update template
DELETE /api/admin/agent-templates/:id — Delete template
```
### User Agents
```
GET /api/agents — List user's agents
POST /api/agents — Create custom agent (or from template)
GET /api/agents/:id — Get agent details
PATCH /api/agents/:id — Update agent (personality, model)
DELETE /api/agents/:id — Delete custom agent
POST /api/agents/:id/chat — Chat with agent (proxy to container)
```
### Agent Status
```
GET /api/agents/status — All agents status for user
GET /api/agents/:id/status — Single agent status
```
## Task Breakdown
| Task ID | Phase | Description | Scope | Dependencies | Estimate |
| -------------- | ------- | ---------------------------------------------- | ----- | ------------ | -------- |
| P2-DB-001 | schema | Prisma models: AgentTemplate, UserAgent | api | P1a | 10K |
| P2-SEED-001 | seed | Seed default agents (jarvis, builder, medic) | api | P2-DB-001 | 5K |
| P2-API-001 | api | Agent template CRUD endpoints | api | P2-DB-001 | 15K |
| P2-API-002 | api | User agent CRUD endpoints | api | P2-DB-001 | 15K |
| P2-API-003 | api | Agent status endpoints | api | P2-DB-001 | 10K |
| P2-PROXY-001 | api | Agent chat routing (select agent by name) | api | P2-API-002 | 15K |
| P2-DISCORD-001 | discord | Route Discord messages to correct agent | api | P2-PROXY-001 | 15K |
| P2-UI-001 | web | Agent list/selector in WebUI | web | P2-API-002 | 15K |
| P2-UI-002 | web | Agent detail/edit page | web | P2-UI-001 | 15K |
| P2-TEST-001 | test | Unit tests for agent services | api | P2-API-002 | 15K |
| P2-VER-001 | verify | End-to-end: Discord → correct agent → response | stack | all | 10K |
**Total Estimate:** ~140K tokens
## Success Criteria
1. ✅ User can list available agents in WebUI
2. ✅ User can select agent and chat with it
3. ✅ Discord messages in #jarvis go to jarvis agent
4. ✅ Discord messages in #builder go to builder agent
5. ✅ Each agent uses its assigned model
6. ✅ Each agent has correct tool permissions
7. ✅ Agents can read/write findings via mosaic skill
## Risks
| Risk | Mitigation |
| --------------------------- | ------------------------------------------------ |
| Agent routing complexity | Keep it simple: map Discord channel → agent name |
| Tool permission enforcement | OpenClaw config generation respects permissions |
| Model fallback failures | Log and alert, don't block user |
## Next Steps
1. Review this PRD with Jason
2. Create Mission MS22-P2 in TASKS.md
3. Begin with P2-DB-001 (schema)

View File

@@ -89,3 +89,20 @@ Design doc: `docs/design/MS22-DB-CENTRIC-ARCHITECTURE.md`
| MS22-P1i | done | phase-1i | Chat proxy: route WebUI chat to user's OpenClaw container (SSE) | — | api+web | feat/ms22-p1i-chat-proxy | P1c,P1d | — | — | — | — | 20K | — | |
| MS22-P1j | done | phase-1j | Docker entrypoint + health checks + core compose | — | docker | feat/ms22-p1j-docker | P1c | — | — | — | — | 10K | — | |
| MS22-P1k | done | phase-1k | Idle reaper cron: stop inactive user containers | — | api | feat/ms22-p1k-idle-reaper | P1d | — | — | — | — | 10K | — | |
## MS22 Phase 2: Named Agent Fleet
PRD: `docs/PRD-MS22-P2-AGENT-FLEET.md`
| Task ID | Status | Phase | Description | Issue | Scope | Branch | Depends On | Blocks | Assigned Worker | Started | Completed | Est Tokens | Act Tokens | Notes |
| ----------- | ----------- | -------- | -------------------------------------------- | -------- | ----- | --------------------------- | ------------- | ------------- | --------------- | ------- | --------- | ---------- | ---------- | ----- |
| MS22-P2-001 | not-started | p2-fleet | Prisma schema: AgentTemplate, UserAgent | TASKS:P2 | api | feat/ms22-p2-agent-schema | MS22-P1a | P2-002,P2-003 | — | — | — | 10K | — | |
| MS22-P2-002 | not-started | p2-fleet | Seed default agents (jarvis, builder, medic) | TASKS:P2 | api | feat/ms22-p2-agent-seed | P2-001 | P2-004 | — | — | — | 5K | — | |
| MS22-P2-003 | not-started | p2-fleet | Agent template CRUD endpoints (admin) | TASKS:P2 | api | feat/ms22-p2-agent-api | P2-001 | P2-005 | — | — | — | 15K | — | |
| MS22-P2-004 | not-started | p2-fleet | User agent CRUD endpoints | TASKS:P2 | api | feat/ms22-p2-agent-api | P2-002,P2-003 | P2-006 | — | — | — | 15K | — | |
| MS22-P2-005 | not-started | p2-fleet | Agent status endpoints | TASKS:P2 | api | feat/ms22-p2-agent-api | P2-003 | P2-008 | — | — | — | 10K | — | |
| MS22-P2-006 | not-started | p2-fleet | Agent chat routing (select agent by name) | TASKS:P2 | api | feat/ms22-p2-agent-routing | P2-004 | P2-007 | — | — | — | 15K | — | |
| MS22-P2-007 | not-started | p2-fleet | Discord channel → agent routing | TASKS:P2 | api | feat/ms22-p2-discord-router | P2-006 | P2-009 | — | — | — | 15K | — | |
| MS22-P2-008 | not-started | p2-fleet | Agent list/selector UI in WebUI | TASKS:P2 | web | feat/ms22-p2-agent-ui | P2-005 | — | — | — | — | 15K | — | |
| MS22-P2-009 | not-started | p2-fleet | Unit tests for agent services | TASKS:P2 | api | test/ms22-p2-agent-tests | P2-007 | P2-010 | — | — | — | 15K | — | |
| MS22-P2-010 | not-started | p2-fleet | E2E verification: Discord → agent → response | TASKS:P2 | stack | — | P2-009 | — | — | — | — | 10K | — | |