Compare commits

..

12 Commits

Author SHA1 Message Date
98e892f23c fix(orchestrator): Dockerfile prisma generate + vitest reflect-metadata setup (#703)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-03-07 04:45:17 +00:00
de6faf659e feat(orchestrator): MS23 agent lifecycle ingestion service (#701)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-03-07 04:21:26 +00:00
49fa958444 chore(orchestrator): MS23 P0-001 done, P0-002 in-progress (#700)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-03-07 02:10:59 +00:00
8d6abd72bb feat(api): MS23 mission control Prisma schema (#699)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-03-07 01:18:06 +00:00
1bed5b3573 chore(orchestrator): Bootstrap MS23 mission-control TASKS.md + PRD (#698)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-03-07 00:27:24 +00:00
c644d1044b Merge pull request 'fix(api): add AuthModule import to UserAgentModule' (#691) from fix/user-agent-auth-module into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
2026-03-05 22:22:18 +00:00
bf5779fb73 fix(api): add AuthModule import to UserAgentModule
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
2026-03-05 16:21:38 -06:00
08d7a6b708 chore: trigger CI rebuild for hotfix 2026-03-05 16:15:32 -06:00
570edef4e5 Merge pull request 'fix(api): add AuthModule import to AgentTemplateModule' (#690) from fix/agent-template-auth-module into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
2026-03-05 22:10:36 +00:00
d220be6b58 fix(api): add AuthModule import to AgentTemplateModule
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
The AgentTemplateController uses AuthGuard which requires AuthService,
but AgentTemplateModule was not importing AuthModule. This caused the
API to crash during dependency resolution.
2026-03-05 09:46:22 -06:00
ade9e968ca chore(ms22-p2): mission complete — all 10 tasks done (#689)
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-03-05 15:41:43 +00:00
413ecdb63b feat(ms22-p2): add Discord channel → agent routing (#688)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com>
Co-committed-by: Jason Woltje <jason@diversecanvas.com>
2026-03-05 14:04:06 +00:00
17 changed files with 1058 additions and 28 deletions

View File

@@ -0,0 +1,83 @@
-- CreateTable
CREATE TABLE "AgentConversationMessage" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"provider" TEXT NOT NULL DEFAULT 'internal',
"role" TEXT NOT NULL,
"content" TEXT NOT NULL,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"metadata" JSONB NOT NULL DEFAULT '{}',
CONSTRAINT "AgentConversationMessage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AgentSessionTree" (
"id" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"parentSessionId" TEXT,
"provider" TEXT NOT NULL DEFAULT 'internal',
"missionId" TEXT,
"taskId" TEXT,
"taskSource" TEXT DEFAULT 'internal',
"agentType" TEXT,
"status" TEXT NOT NULL DEFAULT 'spawning',
"spawnedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completedAt" TIMESTAMP(3),
"metadata" JSONB NOT NULL DEFAULT '{}',
CONSTRAINT "AgentSessionTree_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AgentProviderConfig" (
"id" TEXT NOT NULL,
"workspaceId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"gatewayUrl" TEXT NOT NULL,
"credentials" JSONB NOT NULL DEFAULT '{}',
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AgentProviderConfig_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OperatorAuditLog" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"sessionId" TEXT NOT NULL,
"provider" TEXT NOT NULL,
"action" TEXT NOT NULL,
"content" TEXT,
"metadata" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "OperatorAuditLog_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "AgentConversationMessage_sessionId_timestamp_idx" ON "AgentConversationMessage"("sessionId", "timestamp");
-- CreateIndex
CREATE UNIQUE INDEX "AgentSessionTree_sessionId_key" ON "AgentSessionTree"("sessionId");
-- CreateIndex
CREATE INDEX "AgentSessionTree_parentSessionId_idx" ON "AgentSessionTree"("parentSessionId");
-- CreateIndex
CREATE INDEX "AgentSessionTree_missionId_idx" ON "AgentSessionTree"("missionId");
-- CreateIndex
CREATE UNIQUE INDEX "AgentProviderConfig_workspaceId_name_key" ON "AgentProviderConfig"("workspaceId", "name");
-- CreateIndex
CREATE INDEX "OperatorAuditLog_sessionId_idx" ON "OperatorAuditLog"("sessionId");
-- CreateIndex
CREATE INDEX "OperatorAuditLog_userId_idx" ON "OperatorAuditLog"("userId");
-- CreateIndex
CREATE INDEX "OperatorAuditLog_createdAt_idx" ON "OperatorAuditLog"("createdAt");

View File

@@ -1739,3 +1739,66 @@ model UserAgent {
@@unique([userId, name])
@@index([userId])
}
// MS23: Agent conversation messages for Mission Control streaming
model AgentConversationMessage {
id String @id @default(cuid())
sessionId String
provider String @default("internal")
role String
content String
timestamp DateTime @default(now())
metadata Json @default("{}")
@@index([sessionId, timestamp])
}
// MS23: Agent session tree for parent/child relationships
model AgentSessionTree {
id String @id @default(cuid())
sessionId String @unique
parentSessionId String?
provider String @default("internal")
missionId String?
taskId String?
taskSource String? @default("internal")
agentType String?
status String @default("spawning")
spawnedAt DateTime @default(now())
completedAt DateTime?
metadata Json @default("{}")
@@index([parentSessionId])
@@index([missionId])
}
// MS23: External agent provider configuration per workspace
model AgentProviderConfig {
id String @id @default(cuid())
workspaceId String
name String
provider String
gatewayUrl String
credentials Json @default("{}")
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([workspaceId, name])
}
// MS23: Audit log for operator interventions
model OperatorAuditLog {
id String @id @default(cuid())
userId String
sessionId String
provider String
action String
content String?
metadata Json @default("{}")
createdAt DateTime @default(now())
@@index([sessionId])
@@index([userId])
@@index([createdAt])
}

View File

@@ -2,9 +2,10 @@ import { Module } from "@nestjs/common";
import { AgentTemplateService } from "./agent-template.service";
import { AgentTemplateController } from "./agent-template.controller";
import { PrismaModule } from "../prisma/prisma.module";
import { AuthModule } from "../auth/auth.module";
@Module({
imports: [PrismaModule],
imports: [PrismaModule, AuthModule],
controllers: [AgentTemplateController],
providers: [AgentTemplateService],
exports: [AgentTemplateService],

View File

@@ -2,9 +2,10 @@ import { Module } from "@nestjs/common";
import { UserAgentService } from "./user-agent.service";
import { UserAgentController } from "./user-agent.controller";
import { PrismaModule } from "../prisma/prisma.module";
import { AuthModule } from "../auth/auth.module";
@Module({
imports: [PrismaModule],
imports: [PrismaModule, AuthModule],
controllers: [UserAgentController],
providers: [UserAgentService],
exports: [UserAgentService],

View File

@@ -21,6 +21,8 @@ FROM base AS deps
COPY packages/shared/package.json ./packages/shared/
COPY packages/config/package.json ./packages/config/
COPY apps/orchestrator/package.json ./apps/orchestrator/
# Copy API prisma schema so prisma generate can run in the orchestrator build
COPY apps/api/prisma ./apps/api/prisma
# Copy npm configuration for native binary architecture hints
COPY .npmrc ./
@@ -46,6 +48,10 @@ COPY --from=deps /app/packages/shared/node_modules ./packages/shared/node_module
COPY --from=deps /app/packages/config/node_modules ./packages/config/node_modules
COPY --from=deps /app/apps/orchestrator/node_modules ./apps/orchestrator/node_modules
# Copy API prisma schema and generate the Prisma client for the orchestrator
COPY apps/api/prisma ./apps/api/prisma
RUN pnpm --filter=@mosaic/orchestrator prisma:generate
# Build the orchestrator app using TurboRepo
RUN pnpm turbo build --filter=@mosaic/orchestrator

View File

@@ -3,19 +3,20 @@
"version": "0.0.20",
"private": true,
"scripts": {
"dev": "nest start --watch",
"build": "nest build",
"dev": "nest start --watch",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"prisma:generate": "prisma generate --schema=../api/prisma/schema.prisma",
"start": "node dist/main.js",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:dev": "nest start --watch",
"start:prod": "node dist/main.js",
"test": "vitest",
"test:watch": "vitest watch",
"test:e2e": "vitest run --config tests/integration/vitest.config.ts",
"test:perf": "vitest run --config tests/performance/vitest.config.ts",
"typecheck": "tsc --noEmit",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix"
"test:watch": "vitest watch",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.72.1",
@@ -27,6 +28,7 @@
"@nestjs/core": "^11.1.12",
"@nestjs/platform-express": "^11.1.12",
"@nestjs/throttler": "^6.5.0",
"@prisma/client": "^6.19.2",
"bullmq": "^5.67.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",

View File

@@ -0,0 +1,10 @@
import { Module } from "@nestjs/common";
import { PrismaModule } from "../prisma/prisma.module";
import { AgentIngestionService } from "./agent-ingestion.service";
@Module({
imports: [PrismaModule],
providers: [AgentIngestionService],
exports: [AgentIngestionService],
})
export class AgentIngestionModule {}

View File

@@ -0,0 +1,141 @@
import { Injectable, Logger } from "@nestjs/common";
import type { Prisma } from "@prisma/client";
import { PrismaService } from "../prisma/prisma.service";
export type AgentConversationRole = "agent" | "user" | "system" | "operator";
@Injectable()
export class AgentIngestionService {
private readonly logger = new Logger(AgentIngestionService.name);
constructor(private readonly prisma: PrismaService) {}
private toJsonValue(value: Record<string, unknown>): Prisma.InputJsonValue {
return value as Prisma.InputJsonValue;
}
async recordAgentSpawned(
agentId: string,
parentAgentId?: string,
missionId?: string,
taskId?: string,
agentType?: string
): Promise<void> {
await this.prisma.agentSessionTree.upsert({
where: { sessionId: agentId },
create: {
sessionId: agentId,
parentSessionId: parentAgentId,
missionId,
taskId,
agentType,
status: "spawning",
},
update: {
parentSessionId: parentAgentId,
missionId,
taskId,
agentType,
status: "spawning",
completedAt: null,
},
});
this.logger.debug(`Recorded spawned state for agent ${agentId}`);
}
async recordAgentStarted(agentId: string): Promise<void> {
await this.prisma.agentSessionTree.upsert({
where: { sessionId: agentId },
create: {
sessionId: agentId,
status: "running",
},
update: {
status: "running",
},
});
this.logger.debug(`Recorded running state for agent ${agentId}`);
}
async recordAgentCompleted(agentId: string): Promise<void> {
const completedAt = new Date();
await this.prisma.agentSessionTree.upsert({
where: { sessionId: agentId },
create: {
sessionId: agentId,
status: "completed",
completedAt,
},
update: {
status: "completed",
completedAt,
},
});
this.logger.debug(`Recorded completed state for agent ${agentId}`);
}
async recordAgentFailed(agentId: string, error?: string): Promise<void> {
const completedAt = new Date();
const metadata = error ? this.toJsonValue({ error }) : undefined;
await this.prisma.agentSessionTree.upsert({
where: { sessionId: agentId },
create: {
sessionId: agentId,
status: "failed",
completedAt,
...(metadata && { metadata }),
},
update: {
status: "failed",
completedAt,
...(metadata && { metadata }),
},
});
this.logger.debug(`Recorded failed state for agent ${agentId}`);
}
async recordAgentKilled(agentId: string): Promise<void> {
const completedAt = new Date();
await this.prisma.agentSessionTree.upsert({
where: { sessionId: agentId },
create: {
sessionId: agentId,
status: "killed",
completedAt,
},
update: {
status: "killed",
completedAt,
},
});
this.logger.debug(`Recorded killed state for agent ${agentId}`);
}
async recordMessage(
sessionId: string,
role: AgentConversationRole,
content: string,
provider = "internal",
metadata?: Record<string, unknown>
): Promise<void> {
await this.prisma.agentConversationMessage.create({
data: {
sessionId,
role,
content,
provider,
...(metadata && { metadata: this.toJsonValue(metadata) }),
},
});
this.logger.debug(`Recorded message for session ${sessionId}`);
}
}

View File

@@ -0,0 +1,9 @@
import { Global, Module } from "@nestjs/common";
import { PrismaService } from "./prisma.service";
@Global()
@Module({
providers: [PrismaService],
exports: [PrismaService],
})
export class PrismaModule {}

View File

@@ -0,0 +1,26 @@
import { Injectable, Logger, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";
/**
* Lightweight Prisma service for orchestrator ingestion persistence.
*/
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
private readonly logger = new Logger(PrismaService.name);
constructor() {
super({
log: process.env.NODE_ENV === "development" ? ["warn", "error"] : ["error"],
});
}
async onModuleInit(): Promise<void> {
await this.$connect();
this.logger.log("Database connection established");
}
async onModuleDestroy(): Promise<void> {
await this.$disconnect();
this.logger.log("Database connection closed");
}
}

View File

@@ -1,6 +1,7 @@
import { Injectable, Logger, Inject, forwardRef } from "@nestjs/common";
import { Injectable, Logger, Inject, Optional, forwardRef } from "@nestjs/common";
import { ValkeyService } from "../valkey/valkey.service";
import { AgentSpawnerService } from "./agent-spawner.service";
import { AgentIngestionService } from "../agent-ingestion/agent-ingestion.service";
import type { AgentState, AgentStatus, AgentEvent } from "../valkey/types";
import { isValidAgentTransition } from "../valkey/types/state.types";
@@ -32,7 +33,8 @@ export class AgentLifecycleService {
constructor(
private readonly valkeyService: ValkeyService,
@Inject(forwardRef(() => AgentSpawnerService))
private readonly spawnerService: AgentSpawnerService
private readonly spawnerService: AgentSpawnerService,
@Optional() private readonly agentIngestionService?: AgentIngestionService
) {
this.logger.log("AgentLifecycleService initialized");
}
@@ -55,6 +57,25 @@ export class AgentLifecycleService {
return createdState;
}
private async recordLifecycleIngestion(
agentId: string,
event: "started" | "completed" | "failed" | "killed",
record: (ingestionService: AgentIngestionService) => Promise<void>
): Promise<void> {
if (!this.agentIngestionService) {
return;
}
try {
await record(this.agentIngestionService);
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(
`Failed to record agent ${event} ingestion for ${agentId}: ${errorMessage}`
);
}
}
/**
* Acquire a per-agent mutex to serialize state transitions.
* Uses promise chaining: each caller chains onto the previous lock,
@@ -118,6 +139,10 @@ export class AgentLifecycleService {
// Emit event
await this.publishStateChangeEvent("agent.running", updatedState);
await this.recordLifecycleIngestion(agentId, "started", (ingestionService) =>
ingestionService.recordAgentStarted(agentId)
);
this.logger.log(`Agent ${agentId} transitioned to running`);
return updatedState;
});
@@ -155,6 +180,10 @@ export class AgentLifecycleService {
// Emit event
await this.publishStateChangeEvent("agent.completed", updatedState);
await this.recordLifecycleIngestion(agentId, "completed", (ingestionService) =>
ingestionService.recordAgentCompleted(agentId)
);
// Schedule session cleanup
this.spawnerService.scheduleSessionCleanup(agentId);
@@ -192,6 +221,10 @@ export class AgentLifecycleService {
// Emit event
await this.publishStateChangeEvent("agent.failed", updatedState, error);
await this.recordLifecycleIngestion(agentId, "failed", (ingestionService) =>
ingestionService.recordAgentFailed(agentId, error)
);
// Schedule session cleanup
this.spawnerService.scheduleSessionCleanup(agentId);
@@ -228,6 +261,10 @@ export class AgentLifecycleService {
// Emit event
await this.publishStateChangeEvent("agent.killed", updatedState);
await this.recordLifecycleIngestion(agentId, "killed", (ingestionService) =>
ingestionService.recordAgentKilled(agentId)
);
// Schedule session cleanup
this.spawnerService.scheduleSessionCleanup(agentId);

View File

@@ -1,4 +1,11 @@
import { Injectable, Logger, HttpException, HttpStatus, OnModuleDestroy } from "@nestjs/common";
import {
Injectable,
Logger,
HttpException,
HttpStatus,
OnModuleDestroy,
Optional,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import Anthropic from "@anthropic-ai/sdk";
import { randomUUID } from "crypto";
@@ -8,6 +15,7 @@ import {
AgentSession,
AgentType,
} from "./types/agent-spawner.types";
import { AgentIngestionService } from "../agent-ingestion/agent-ingestion.service";
/**
* Default delay in milliseconds before cleaning up sessions after terminal states
@@ -30,7 +38,10 @@ export class AgentSpawnerService implements OnModuleDestroy {
private readonly sessionCleanupDelayMs: number;
private readonly cleanupTimers = new Map<string, NodeJS.Timeout>();
constructor(private readonly configService: ConfigService) {
constructor(
private readonly configService: ConfigService,
@Optional() private readonly agentIngestionService?: AgentIngestionService
) {
const configuredProvider = this.configService.get<string>("orchestrator.aiProvider");
this.aiProvider = this.normalizeAiProvider(configuredProvider);
@@ -98,6 +109,19 @@ export class AgentSpawnerService implements OnModuleDestroy {
this.cleanupTimers.clear();
}
private recordSpawnedAgentIngestion(agentId: string, request: SpawnAgentRequest): void {
if (!this.agentIngestionService) {
return;
}
void this.agentIngestionService
.recordAgentSpawned(agentId, undefined, undefined, request.taskId, request.agentType)
.catch((error: unknown) => {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`Failed to record spawned ingestion for ${agentId}: ${errorMessage}`);
});
}
/**
* Spawn a new agent with the given configuration
* @param request Agent spawn request
@@ -130,6 +154,8 @@ export class AgentSpawnerService implements OnModuleDestroy {
// Store session
this.sessions.set(agentId, session);
this.recordSpawnedAgentIngestion(agentId, request);
this.logger.log(`Agent spawned successfully: ${agentId} (type: ${request.agentType})`);
// NOTE: Actual Claude SDK integration will be implemented in next iteration (see issue #TBD)

View File

@@ -3,9 +3,10 @@ import { AgentSpawnerService } from "./agent-spawner.service";
import { AgentLifecycleService } from "./agent-lifecycle.service";
import { DockerSandboxService } from "./docker-sandbox.service";
import { ValkeyModule } from "../valkey/valkey.module";
import { AgentIngestionModule } from "../agent-ingestion/agent-ingestion.module";
@Module({
imports: [ValkeyModule],
imports: [ValkeyModule, AgentIngestionModule],
providers: [AgentSpawnerService, AgentLifecycleService, DockerSandboxService],
exports: [AgentSpawnerService, AgentLifecycleService, DockerSandboxService],
})

View File

@@ -4,6 +4,7 @@ export default defineConfig({
test: {
globals: true,
environment: "node",
setupFiles: ["reflect-metadata"],
exclude: ["**/node_modules/**", "**/dist/**", "**/tests/integration/**"],
include: ["src/**/*.spec.ts", "src/**/*.test.ts"],
coverage: {

View File

@@ -0,0 +1,555 @@
# PRD: MS23 — Mission Control Dashboard & Agent Provider Interface
## Metadata
- **Owner:** Jason Woltje
- **Date:** 2026-03-06
- **Status:** draft
- **Mission ID:** ms23-mission-control-20260306
- **Target Version:** 0.0.23
- **Roadmap Milestone:** M6 — Orchestration (0.0.6 trajectory)
- **Depends On:** MS22 Phase 2 (Named Agent Fleet) — COMPLETE
- **Related Docs:**
- `~/src/jarvis-brain/docs/planning/MISSION-CONTROL-UI-PRD.md` (concept origin)
- `~/src/jarvis-brain/docs/planning/FLEET-EVOLUTION-PLAN.md`
- `docs/PRD-MS22-P2-AGENT-FLEET.md`
---
## Problem Statement
The Mosaic orchestration backend is fully operational: agents spawn, execute tasks, publish lifecycle events via Valkey pub/sub, and can be killed via API. The frontend exposes rudimentary widgets (AgentStatusWidget, OrchestratorEventsWidget) that show aggregate status.
What's missing is **operational visibility and control at the session level**. There is no way to:
1. See what an individual agent is actually saying and doing (conversation stream per agent)
2. Inject a message into a running agent session without terminating it (barge-in)
3. Understand the parent/child relationship between orchestrators and their subagents
4. Connect Mosaic's orchestration layer to external agent runtimes (OpenClaw sessions, Codex ACP, raw PTY agents) through a consistent, extensible interface
Jason operates multiple projects in parallel — multiple orchestrating agents running simultaneously across missions. Today this requires context-switching between terminals, Discord channels, and status widgets. Mission Control solves this.
**Mosaic is designed to be an enterprise-grade, multi-user AI operations platform.** Not every user will use OpenClaw. Not every team will use Codex. Mosaic must provide a plugin adapter interface that allows any agent runtime to integrate with the same orchestration harness, control plane, and UI.
---
## Objectives
1. **Mission Control Dashboard** — Single-pane-of-glass view: N orchestrator panels in a responsive grid, each showing a live agent chat stream with full operator controls
2. **Per-Agent Conversation Streaming** — Stream individual agent message logs (not just lifecycle events) to the frontend via SSE
3. **Barge-In / Message Injection** — Operator can inject messages directly into any running agent session with audit trail
4. **Subagent Tree Tracking** — Agents report parent/child relationships; UI renders the full agent roster as a tree
5. **Agent Provider Interface (API)** — Formal plugin adapter interface that any agent runtime can implement to integrate with Mosaic's orchestration layer
6. **OpenClaw Provider Adapter** — Reference implementation of the Agent Provider Interface for OpenClaw ACP sessions
7. **Operator Controls** — Pause, resume, graceful terminate, hard kill per agent; kill-all panic button
8. **Audit Trail** — All operator interventions (barge-in, kill, pause) logged with timestamp, user, target, and content
---
## Scope
### In Scope
- Agent conversation log storage and streaming API (per-agent SSE stream of messages)
- Barge-in endpoint: inject operator message into running agent session
- Pause / resume agent execution
- Subagent tree: parent-agent relationship on spawn registration
- Agent Provider Interface: TypeScript interface + NestJS plugin module
- OpenClaw adapter: implements Agent Provider Interface for OpenClaw sessions
- Mission Control page (`/mission-control`) with grid of orchestrator panels
- OrchestratorPanel component: live chat stream + barge-in input + operator controls
- Global Agent Roster: tree view sidebar showing all agents + subagents with kill buttons
- Audit log: UI and API for operator action history
- Role: `operator` (full control) and `observer` (read-only) applied to all new endpoints
### Out of Scope
- Mobile layout (desktop-first, responsive grid min-width 1200px)
- Multi-user concurrent barge-in coordination (single operator per session)
- Historical session replay / time-travel debugging (future milestone)
- Codex ACP adapter (follow-on after OpenClaw adapter validates interface)
- Raw PTY adapter (follow-on)
- Agent-to-agent communication graph visualization (future)
- Agent marketplace / plugin registry UI (future)
---
## Current State Assessment
### What Exists (Do Not Rebuild)
| Component | Location | Status |
| ------------------------ | --------------------------------------- | -------------------------------- |
| AgentSpawnerService | `apps/orchestrator/src/spawner/` | ✅ Production |
| AgentLifecycleService | `apps/orchestrator/src/spawner/` | ✅ Production |
| KillswitchService | `apps/orchestrator/src/killswitch/` | ✅ Production |
| AgentEventsService | `apps/orchestrator/src/api/agents/` | ✅ SSE lifecycle events |
| `GET /agents` | Orchestrator API | ✅ Lists all agents |
| `POST /agents/:id/kill` | Orchestrator API | ✅ Kills agent |
| `POST /agents/kill-all` | Orchestrator API | ✅ Kills all |
| `GET /agents/events` | Orchestrator API | ✅ SSE lifecycle stream |
| AgentStatusWidget | `apps/web/src/components/widgets/` | ✅ Polls agent list |
| OrchestratorEventsWidget | `apps/web/src/components/widgets/` | ✅ SSE lifecycle events |
| HUD widget grid | `apps/web/src/components/hud/` | ✅ Drag/resize/add/remove |
| Chat component | `apps/web/src/components/chat/` | ✅ Chat UI exists |
| Socket.io | `apps/api/` (speech.gateway.ts) | ✅ WebSocket pattern established |
| CoordinatorIntegration | `apps/api/src/coordinator-integration/` | ✅ API ↔ Orchestrator bridge |
### What's Missing (Build This)
| Gap | Priority |
| ------------------------------------------------ | -------- |
| Per-agent conversation message log (DB + API) | P0 |
| Per-agent SSE message stream | P0 |
| Barge-in endpoint (`POST /agents/:id/inject`) | P0 |
| Pause / resume endpoints | P1 |
| Subagent tree (parentAgentId on registration) | P0 |
| Agent Provider Interface (plugin API) | P0 |
| OpenClaw adapter (implements provider interface) | P1 |
| Mission Control page (`/mission-control`) | P0 |
| OrchestratorPanel component | P0 |
| Global Agent Roster (tree view) | P0 |
| Audit log (DB + API + UI) | P1 |
---
## Architecture
### Agent Provider Interface
Mosaic defines a standard contract. Any agent runtime that implements this interface integrates natively with Mission Control.
```typescript
// packages/shared/src/agent-provider.interface.ts
export interface AgentSession {
sessionId: string;
parentSessionId?: string; // For subagent tree
provider: string; // "internal" | "openclaw" | "codex" | ...
status: AgentSessionStatus;
taskId?: string;
missionId?: string;
agentType?: string;
spawnedAt: string;
startedAt?: string;
completedAt?: string;
error?: string;
metadata?: Record<string, unknown>;
}
export type AgentSessionStatus =
| "spawning"
| "running"
| "waiting"
| "paused"
| "completed"
| "failed"
| "killed";
export interface AgentMessage {
messageId: string;
sessionId: string;
role: "agent" | "user" | "system" | "operator";
content: string;
timestamp: string;
metadata?: Record<string, unknown>;
}
export interface IAgentProvider {
readonly providerName: string;
/** List all currently active sessions */
listSessions(): Promise<AgentSession[]>;
/** Get a single session's current state */
getSession(sessionId: string): Promise<AgentSession | null>;
/** Get recent messages for a session */
getMessages(sessionId: string, limit?: number): Promise<AgentMessage[]>;
/** Subscribe to a session's message stream. Returns unsubscribe fn. */
subscribeToMessages(sessionId: string, handler: (message: AgentMessage) => void): () => void;
/** Inject an operator message into a running session (barge-in) */
injectMessage(sessionId: string, content: string, operatorId: string): Promise<void>;
/** Pause a running agent session */
pause(sessionId: string): Promise<void>;
/** Resume a paused agent session */
resume(sessionId: string): Promise<void>;
/** Graceful terminate — allow agent to finish current step */
terminate(sessionId: string): Promise<void>;
/** Hard kill — immediate termination */
kill(sessionId: string): Promise<void>;
}
```
### Internal Provider
The existing orchestrator's Docker-based agents implement `IAgentProvider` as the "internal" provider. No behavior change — just wraps existing services behind the interface.
### OpenClaw Provider
Connects to an OpenClaw gateway via its REST API:
- `GET /sessions``listSessions()`
- `GET /sessions/:key/history``getMessages()`
- `POST /sessions/:key/send``injectMessage()`
- OpenClaw SSE or polling → `subscribeToMessages()`
Config per workspace in DB (`AgentProvider` table): gateway URL, API token.
### Provider Registry
```typescript
// apps/api/src/agent-providers/provider-registry.service.ts
@Injectable()
export class AgentProviderRegistry {
register(provider: IAgentProvider): void;
getProvider(name: string): IAgentProvider;
getAllProviders(): IAgentProvider[];
listAllSessions(): Promise<AgentSession[]>; // Aggregates across all providers
}
```
---
## Database Schema
### New Tables
```prisma
// AgentConversationMessage — stores all agent messages for streaming + history
model AgentConversationMessage {
id String @id @default(cuid())
sessionId String // matches agentId in orchestrator
provider String @default("internal") // "internal" | "openclaw" | ...
role String // "agent" | "user" | "system" | "operator"
content String
timestamp DateTime @default(now())
metadata Json @default("{}")
@@index([sessionId, timestamp])
}
// AgentSessionTree — tracks parent/child relationships
model AgentSessionTree {
id String @id @default(cuid())
sessionId String @unique
parentSessionId String?
provider String @default("internal")
missionId String?
taskId String?
agentType String?
status String @default("spawning")
spawnedAt DateTime @default(now())
completedAt DateTime?
metadata Json @default("{}")
@@index([parentSessionId])
@@index([missionId])
}
// AgentProviderConfig — external provider registration per workspace
model AgentProviderConfig {
id String @id @default(cuid())
workspaceId String
name String // "openclaw-prod", "codex-team", ...
provider String // "openclaw" | "codex" | ...
gatewayUrl String
credentials Json @default("{}") // Encrypted via CryptoService
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([workspaceId, name])
}
// OperatorAuditLog — all operator interventions
model OperatorAuditLog {
id String @id @default(cuid())
userId String
sessionId String
provider String
action String // "barge-in" | "kill" | "pause" | "resume" | "kill-all"
content String? // For barge-in: message injected
metadata Json @default("{}")
createdAt DateTime @default(now())
@@index([sessionId])
@@index([userId])
@@index([createdAt])
}
```
---
## API Endpoints
### Orchestrator API — New Endpoints
```
POST /agents/:agentId/inject — Barge-in: inject operator message
POST /agents/:agentId/pause — Pause agent execution
POST /agents/:agentId/resume — Resume paused agent
GET /agents/:agentId/messages — Get message history (paginated)
GET /agents/:agentId/messages/stream — SSE: live message stream for this agent
GET /agents/tree — Full subagent tree (all agents with parent/child)
```
### Main API — New Endpoints
```
# Agent Provider Management
GET /api/agent-providers — List configured providers
POST /api/agent-providers — Register external provider
PATCH /api/agent-providers/:id — Update provider config
DELETE /api/agent-providers/:id — Remove provider
# Unified Session View (aggregates all providers)
GET /api/mission-control/sessions — All active sessions (all providers)
GET /api/mission-control/sessions/:id — Single session details
GET /api/mission-control/sessions/:id/messages — Message history
GET /api/mission-control/sessions/:id/stream — SSE message stream (proxied)
POST /api/mission-control/sessions/:id/inject — Barge-in (proxied to provider)
POST /api/mission-control/sessions/:id/pause — Pause (proxied)
POST /api/mission-control/sessions/:id/resume — Resume (proxied)
POST /api/mission-control/sessions/:id/kill — Kill (proxied)
GET /api/mission-control/tree — Full agent tree (all providers)
GET /api/mission-control/audit — Operator audit log (paginated)
```
### Authorization
All Mission Control endpoints require auth + workspace context.
- `operator` role: full access (read + inject + kill + pause)
- `observer` role: read-only (no inject, no kill, no pause)
- `admin` role: full access + provider config management
---
## Frontend — Mission Control Page
### Route
`/mission-control` — new top-level page in the web app, linked in sidebar under "Orchestration"
### Layout
```
┌─────────────────────────────────────────────────────────────────┐
│ ⚙ MISSION CONTROL [+ Add Panel] [🔴 KILL ALL] │
├──────────────────────────────────────┬──────────────────────────┤
│ │ ACTIVE AGENTS │
│ ┌──────────────┬──────────────┐ │ ▼ ms22 [internal] 🟢 │
│ │ [Panel: ms22]│ [Panel: SAGE]│ │ ├ codex-1 task-api 🟢 │
│ │ 🟢 3 agents │ 🟡 1 agent │ │ ├ codex-2 task-ui 🟢 │
│ │ │ │ │ └ glm-1 task-db 🟡 │
│ │ [chat stream]│ [chat stream]│ │ ▼ SAGE [openclaw] 🟢 │
│ │ │ │ │ └ codex-1 task-prd 🟢 │
│ │ [input ▶] │ [input ▶] │ │ │
│ │ [⚡][⏸][💀] │ [⚡][⏸][💀] │ │ [⏸ pause] [💀 kill] per │
│ └──────────────┴──────────────┘ │ agent │
│ │ │
│ [+ Add Orchestrator Panel] │ [📋 Audit Log] │
└──────────────────────────────────────┴──────────────────────────┘
```
### Components
**MissionControlPage** (`/app/mission-control/page.tsx`)
- Fetches active sessions from `/api/mission-control/sessions`
- Renders N `OrchestratorPanel` in a responsive CSS grid
- Sidebar: `GlobalAgentRoster`
- Header: session count, Kill All button (confirm dialog)
**OrchestratorPanel** (`components/mission-control/OrchestratorPanel.tsx`)
- Props: `sessionId`, `provider`, `title`
- Subscribes to `/api/mission-control/sessions/:id/stream` (SSE)
- Renders scrollable message list (role-tagged, styled by role)
- Input box + Send button (barge-in → `POST /inject`)
- Header: status badge, agent count, elapsed time, ⚡ Barge-In toggle, ⏸ Pause, 💀 Kill
- Expandable to full-screen (modal overlay)
- Color-coded border by status (green/yellow/red/gray)
**GlobalAgentRoster** (`components/mission-control/GlobalAgentRoster.tsx`)
- Fetches `/api/mission-control/tree`
- Renders tree: orch session → indented subagents
- Per-row: provider badge, status dot, task label, elapsed, Kill button
- Real-time updates via polling or SSE events
**BargeInInput** (`components/mission-control/BargeInInput.tsx`)
- Elevated textarea that renders inside a panel
- "Pause before send" checkbox
- Sends to `POST /inject`, shows confirmation
**AuditLogDrawer** (`components/mission-control/AuditLogDrawer.tsx`)
- Slide-in drawer from right
- Paginated table: timestamp, user, action, session, content preview
- Triggered from sidebar "Audit Log" button
**KillAllDialog** (`components/mission-control/KillAllDialog.tsx`)
- Confirmation modal with provider scope selector
- "Kill all internal agents" / "Kill all (all providers)"
- Requires typing "KILL ALL" to confirm
---
## Implementation Phases
### Phase 0 — Foundation (Backend Core)
Backend infrastructure required before any UI work.
| Task | Description | Scope | Est |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------ | --- |
| MS23-P0-001 | Prisma schema: AgentConversationMessage, AgentSessionTree, AgentProviderConfig, OperatorAuditLog — see mosaic-queue note below | api | 15K |
| MS23-P0-002 | Agent message ingestion: wire spawner/lifecycle to write messages to DB | orchestrator | 20K |
| MS23-P0-003 | Orchestrator API: `GET /agents/:id/messages` + SSE stream endpoint | orchestrator | 20K |
| MS23-P0-004 | Orchestrator API: `POST /agents/:id/inject` + pause/resume | orchestrator | 15K |
| MS23-P0-005 | Subagent tree: `parentAgentId` on spawn registration + `GET /agents/tree` | orchestrator | 15K |
| MS23-P0-006 | Unit + integration tests for all P0 orchestrator endpoints | orchestrator | 20K |
**Phase 0 gate:** All orchestrator endpoints tested and green. Per-agent message stream verified via curl/SSE client.
> **mosaic-queue Integration Note**
>
> `mosaic-queue` (`~/src/mosaic-queue`) is a standalone Valkey-backed task registry (CLI + MCP server) that agents use to claim and complete tasks in a pull model. It is complementary to — not a replacement for — the orchestrator's internal `QueueService` (which is push-based agent dispatch).
>
> **Schema impact on MS23-P0-001:**
>
> - `AgentSessionTree.taskId` should be `String?` and optionally reference a mosaic-queue task key
> - Add `AgentSessionTree.taskSource String? @default("internal")` — values: `"internal"` | `"mosaic-queue"` | `"external"`
> - This allows Mission Control's agent roster to resolve task metadata (title, priority, status) from the correct source
>
> **Future integration point:**
> mosaic-queue Phase 3 ("coordinator integration") will wire the coordinator to claim tasks from mosaic-queue and spawn orchestrator agents against them. When that ships, Mission Control will inherit rich task context (title, lane, priority, retry count) from the queue automatically — no rework needed in MS23's data model if `taskSource` is present from the start.
>
> **No blocking dependency:** mosaic-queue Phase 3 is not required for MS23. The `taskSource` field is additive and can be `null` initially.
### Phase 1 — Provider Interface (Plugin Architecture)
| Task | Description | Scope | Est |
| ----------- | ---------------------------------------------------------------------------------------------------------------- | ------ | --- |
| MS23-P1-001 | `IAgentProvider` interface + shared types in `packages/shared` | shared | 10K |
| MS23-P1-002 | `InternalAgentProvider`: wrap existing orchestrator services behind interface | api | 20K |
| MS23-P1-003 | `AgentProviderRegistry`: register/retrieve providers, aggregate listSessions | api | 15K |
| MS23-P1-004 | `AgentProviderConfig` CRUD API (`/api/agent-providers`) | api | 15K |
| MS23-P1-005 | Mission Control proxy API (`/api/mission-control/*`): routes to registry, handles SSE proxying, writes audit log | api | 30K |
| MS23-P1-006 | Unit tests for registry, proxy service, internal provider | api | 20K |
**Phase 1 gate:** Unified `/api/mission-control/sessions` returns sessions from internal provider. Proxy routes correctly to internal provider for kill/pause/inject. Audit log persisted.
### Phase 2 — Mission Control UI
| Task | Description | Scope | Est |
| ----------- | ----------------------------------------------------------------------- | ----- | --- |
| MS23-P2-001 | `/mission-control` page route + layout shell | web | 10K |
| MS23-P2-002 | `OrchestratorPanel` component: SSE message stream, chat display | web | 25K |
| MS23-P2-003 | `BargeInInput` component: inject message, pause-before-send | web | 15K |
| MS23-P2-004 | Panel operator controls: pause, resume, graceful kill, hard kill | web | 15K |
| MS23-P2-005 | `GlobalAgentRoster` sidebar: tree view, per-agent kill | web | 20K |
| MS23-P2-006 | `KillAllDialog`: confirmation modal with scope selector | web | 10K |
| MS23-P2-007 | `AuditLogDrawer`: paginated audit history | web | 15K |
| MS23-P2-008 | Panel grid: responsive layout, add/remove panels, expand to full-screen | web | 20K |
| MS23-P2-009 | Frontend tests (vitest + Playwright E2E for mission control page) | web | 25K |
**Phase 2 gate:** Mission Control page renders with live panels. Barge-in sends and displays. Kill triggers confirmation and removes agent from roster. Audit log shows entries. All tests green.
### Phase 3 — OpenClaw Provider Adapter
| Task | Description | Scope | Est |
| ----------- | ---------------------------------------------------------------------------------- | ------- | --- |
| MS23-P3-001 | `OpenClawProvider`: implement `IAgentProvider` against OpenClaw REST API | api | 25K |
| MS23-P3-002 | OpenClaw session polling / SSE bridge: translate OpenClaw events to `AgentMessage` | api | 20K |
| MS23-P3-003 | Provider config UI: register OpenClaw gateway (URL + API token) in Settings | web | 15K |
| MS23-P3-004 | E2E test: OpenClaw provider registered → sessions appear in Mission Control | api+web | 20K |
**Phase 3 gate:** OpenClaw sessions visible in Mission Control alongside internal agents. Barge-in to OpenClaw session injects message and shows in panel stream.
### Phase 4 — Verification & Release
| Task | Description | Scope | Est |
| ----------- | --------------------------------------------------------------------------------------- | ----- | --- |
| MS23-P4-001 | Full QA: all gates (lint, typecheck, unit, E2E) | stack | 10K |
| MS23-P4-002 | Security review: auth on all new endpoints, audit log integrity, barge-in rate limiting | api | 10K |
| MS23-P4-003 | Deploy to production (mosaic.woltje.com), smoke test with live agents | stack | 5K |
| MS23-P4-004 | Update ROADMAP.md + CHANGELOG.md, tag v0.0.23 | stack | 3K |
---
## Completion Gates (Mandatory)
Per Mosaic E2E delivery framework — a task is NOT done until:
- [ ] Code review (independent review of every changed file)
- [ ] Security review (auth, input validation, error leakage)
- [ ] QA / tests green (`pnpm turbo lint typecheck test`)
- [ ] CI pipeline green after merge
- [ ] Gitea issue closed
- [ ] Docs updated for any API or schema changes
---
## Token Budget Estimate
| Phase | Tasks | Estimate |
| ---------------------------- | ------ | --------- |
| Phase 0 — Backend Core | 6 | ~105K |
| Phase 1 — Provider Interface | 6 | ~110K |
| Phase 2 — Mission Control UI | 9 | ~155K |
| Phase 3 — OpenClaw Adapter | 4 | ~80K |
| Phase 4 — Verification | 4 | ~28K |
| **Total** | **29** | **~478K** |
Recommended split: Codex for UI (Phase 2) and routine API work. Sonnet for provider interface design and complex streaming logic.
---
## Security Considerations
- All Mission Control endpoints require authenticated session + workspace membership
- Barge-in rate-limited: 10 requests/minute per operator per session
- Kill All requires explicit confirmation (UI + double-confirm pattern)
- External provider credentials stored encrypted (AES-256-GCM via CryptoService)
- Audit log is append-only; no delete endpoint
- SSE streams authenticated via session cookie (no unauthenticated streams)
- Operator actions tagged with userId for full traceability
- `observer` role enforced at middleware level — cannot be bypassed by frontend
---
## Open Questions
1. **Panel persistence:** Should the grid layout (which sessions are pinned as panels) be stored in DB per user or in localStorage? Recommend DB for cross-device consistency.
2. **Message retention:** How long to keep `AgentConversationMessage` records? Suggest 30-day default with configurable workspace policy.
3. **OpenClaw barge-in protocol:** Does OpenClaw's `sessions_send` API support injection mid-run, or does it queue behind the current turn? Needs verification against OpenClaw API before MS23-P3-001.
4. **Subagent reporting:** Internal agents currently don't self-report a `parentAgentId` at spawn time. The orchestrator spawner needs to accept this field. Straightforward add to `SpawnAgentDto`.
5. **SSE vs WebSocket for message streaming:** Current orchestrator uses SSE (one-way push). For barge-in confirmation/ack, SSE is sufficient (inject is a separate REST call). No need to upgrade to bidirectional WebSocket for Phase 0-2.
6. **mosaic-queue Phase 3 timing:** mosaic-queue's coordinator integration phase is not yet scheduled. If it ships during MS23 development, the `taskSource` field in `AgentSessionTree` is the integration point — no schema migration required. The Mission Control roster can conditionally render task details from mosaic-queue when `taskSource === "mosaic-queue"` and the queue MCP/API is reachable.
---
## Success Criteria
1. Operator can open Mission Control and see all running orchestrator sessions as live panels
2. Each panel shows the agent's actual conversation messages in real time
3. Operator can type into any panel and inject a message; it appears in the stream tagged `[OPERATOR]`
4. Operator can pause, resume, gracefully terminate, or hard-kill any agent from the panel or roster
5. Global Agent Roster shows the full parent → subagent tree across all providers
6. Kill All button with confirmation terminates all active agents
7. All operator actions appear in the Audit Log with full attribution
8. OpenClaw sessions registered as an external provider appear in Mission Control alongside internal agents
9. `observer` role users can see everything but cannot inject, pause, or kill
10. All CI gates green, deployed to production

View File

@@ -106,3 +106,80 @@ PRD: `docs/PRD-MS22-P2-AGENT-FLEET.md`
| MS22-P2-008 | done | p2-fleet | Agent list/selector UI in WebUI | TASKS:P2 | web | feat/ms22-p2-agent-ui | P2-005 | — | orchestrator | 2026-03-04 | 2026-03-04 | 15K | 8K | PR #685 merged |
| MS22-P2-009 | done | p2-fleet | Unit tests for agent services | TASKS:P2 | api | test/ms22-p2-agent-tests | P2-006 | P2-010 | orchestrator | 2026-03-04 | 2026-03-05 | 15K | 8K | PR #687 merged |
| MS22-P2-010 | done | p2-fleet | E2E verification: Discord → agent → response | TASKS:P2 | stack | — | P2-009 | — | orchestrator | 2026-03-05 | 2026-03-05 | 10K | 5K | All gates green |
---
## MS23 — Mission Control Dashboard & Agent Provider Interface
PRD: `docs/PRD-MS23-mission-control.md`
Milestone: `0.0.23`
Target version: `v0.0.23`
> Single-writer: orchestrator (Jarvis/OpenClaw) only. Workers read but never modify.
### Phase 0 — Backend Core (Foundation)
| id | status | milestone | description | issue | repo | branch | depends_on | blocks | agent | started_at | completed_at | estimate | used | notes |
| ----------- | ----------- | ------------- | ------------------------------------------------------------------------------------------------ | ----- | ------------ | ---------------------- | ----------------------------------------------- | ----------------------------------------------------------- | ----- | ---------- | ------------ | -------- | ---- | --------------------------------------------- |
| MS23-P0-001 | done | p0-foundation | Prisma schema: AgentConversationMessage, AgentSessionTree, AgentProviderConfig, OperatorAuditLog | #693 | api | feat/ms23-p0-schema | — | MS23-P0-002,MS23-P0-003,MS23-P0-004,MS23-P0-005,MS23-P1-001 | codex | 2026-03-06 | 2026-03-06 | 15K | — | taskSource field per mosaic-queue note in PRD |
| MS23-P0-002 | in-progress | p0-foundation | Agent message ingestion: wire spawner/lifecycle to write messages to DB | #693 | orchestrator | feat/ms23-p0-ingestion | MS23-P0-001 | MS23-P0-006 | codex | 2026-03-06 | — | 20K | — | |
| MS23-P0-003 | not-started | p0-foundation | Orchestrator API: GET /agents/:id/messages + SSE stream endpoint | #693 | orchestrator | feat/ms23-p0-stream | MS23-P0-001 | MS23-P0-006 | — | — | — | 20K | — | |
| MS23-P0-004 | not-started | p0-foundation | Orchestrator API: POST /agents/:id/inject + pause/resume endpoints | #693 | orchestrator | feat/ms23-p0-controls | MS23-P0-001 | MS23-P0-006 | — | — | — | 15K | — | |
| MS23-P0-005 | not-started | p0-foundation | Subagent tree: parentAgentId on spawn registration + GET /agents/tree | #693 | orchestrator | feat/ms23-p0-tree | MS23-P0-001 | MS23-P0-006 | — | — | — | 15K | — | |
| MS23-P0-006 | not-started | p0-foundation | Unit + integration tests for all P0 orchestrator endpoints | #693 | orchestrator | test/ms23-p0 | MS23-P0-002,MS23-P0-003,MS23-P0-004,MS23-P0-005 | MS23-P1-001 | — | — | — | 20K | — | Phase 0 gate: SSE stream verified via curl |
### Phase 1 — Provider Interface (Plugin Architecture)
| id | status | milestone | description | issue | repo | branch | depends_on | blocks | agent | started_at | completed_at | estimate | used | notes |
| ----------- | ----------- | ----------- | ----------------------------------------------------------------------------------- | ----- | ------ | ------------------------------ | ----------------------------------- | ----------------------- | ----- | ---------- | ------------ | -------- | ---- | ------------------------------------------------------------------ |
| MS23-P1-001 | not-started | p1-provider | IAgentProvider interface + AgentSession/AgentMessage types in packages/shared | #694 | shared | feat/ms23-p1-interface | MS23-P0-001,MS23-P0-006 | MS23-P1-002,MS23-P1-003 | — | — | — | 10K | — | |
| MS23-P1-002 | not-started | p1-provider | InternalAgentProvider: wrap existing orchestrator services behind IAgentProvider | #694 | api | feat/ms23-p1-internal-provider | MS23-P1-001 | MS23-P1-003,MS23-P1-006 | — | — | — | 20K | — | |
| MS23-P1-003 | not-started | p1-provider | AgentProviderRegistry: register/retrieve providers, aggregate listSessions | #694 | api | feat/ms23-p1-registry | MS23-P1-001,MS23-P1-002 | MS23-P1-004,MS23-P1-005 | — | — | — | 15K | — | |
| MS23-P1-004 | not-started | p1-provider | AgentProviderConfig CRUD API (/api/agent-providers) | #694 | api | feat/ms23-p1-provider-api | MS23-P1-003 | MS23-P1-006 | — | — | — | 15K | — | |
| MS23-P1-005 | not-started | p1-provider | Mission Control proxy API (/api/mission-control/\*): SSE proxying, audit log writes | #694 | api | feat/ms23-p1-proxy | MS23-P1-003 | MS23-P1-006,MS23-P2-001 | — | — | — | 30K | — | Aggregates all providers; most complex P1 task |
| MS23-P1-006 | not-started | p1-provider | Unit tests: registry, proxy service, internal provider | #694 | api | test/ms23-p1 | MS23-P1-002,MS23-P1-004,MS23-P1-005 | MS23-P2-001 | — | — | — | 20K | — | Phase 1 gate: unified /sessions returns internal provider sessions |
### Phase 2 — Mission Control UI
| id | status | milestone | description | issue | repo | branch | depends_on | blocks | agent | started_at | completed_at | estimate | used | notes |
| ----------- | ----------- | --------- | ----------------------------------------------------------------------------------- | ----- | ---- | --------------------- | ----------------------------------------------------------------------- | ----------------------- | ----- | ---------- | ------------ | -------- | ---- | ------------------------------------- |
| MS23-P2-001 | not-started | p2-ui | /mission-control page route + layout shell (sidebar + panel grid) | #695 | web | feat/ms23-p2-page | MS23-P1-005,MS23-P1-006 | MS23-P2-002,MS23-P2-005 | — | — | — | 10K | — | |
| MS23-P2-002 | not-started | p2-ui | OrchestratorPanel component: SSE message stream, chat display, role-tagged messages | #695 | web | feat/ms23-p2-panel | MS23-P2-001 | MS23-P2-003,MS23-P2-004 | — | — | — | 25K | — | |
| MS23-P2-003 | not-started | p2-ui | BargeInInput component: inject message, pause-before-send checkbox | #695 | web | feat/ms23-p2-barge | MS23-P2-002 | MS23-P2-009 | — | — | — | 15K | — | |
| MS23-P2-004 | not-started | p2-ui | Panel operator controls: pause, resume, graceful kill, hard kill buttons | #695 | web | feat/ms23-p2-controls | MS23-P2-002 | MS23-P2-009 | — | — | — | 15K | — | |
| MS23-P2-005 | not-started | p2-ui | GlobalAgentRoster sidebar: tree view, per-agent kill buttons | #695 | web | feat/ms23-p2-roster | MS23-P2-001 | MS23-P2-006,MS23-P2-009 | — | — | — | 20K | — | |
| MS23-P2-006 | not-started | p2-ui | KillAllDialog: confirmation modal with scope selector (internal / all providers) | #695 | web | feat/ms23-p2-killall | MS23-P2-005 | MS23-P2-009 | — | — | — | 10K | — | Requires typing "KILL ALL" to confirm |
| MS23-P2-007 | not-started | p2-ui | AuditLogDrawer: paginated audit history slide-in drawer | #695 | web | feat/ms23-p2-audit | MS23-P2-001 | MS23-P2-009 | — | — | — | 15K | — | |
| MS23-P2-008 | not-started | p2-ui | Panel grid: responsive layout, add/remove panels, full-screen expand | #695 | web | feat/ms23-p2-grid | MS23-P2-002 | MS23-P2-009 | — | — | — | 20K | — | |
| MS23-P2-009 | not-started | p2-ui | Frontend tests: vitest unit + Playwright E2E for mission control page | #695 | web | test/ms23-p2 | MS23-P2-003,MS23-P2-004,MS23-P2-005,MS23-P2-006,MS23-P2-007,MS23-P2-008 | MS23-P3-001 | — | — | — | 25K | — | Phase 2 gate |
### Phase 3 — OpenClaw Provider Adapter
| id | status | milestone | description | issue | repo | branch | depends_on | blocks | agent | started_at | completed_at | estimate | used | notes |
| ----------- | ----------- | ----------- | -------------------------------------------------------------------------------- | ----- | ------- | ------------------------------ | ----------- | ----------- | ----- | ---------- | ------------ | -------- | ---- | --------------------------------------------------- |
| MS23-P3-001 | not-started | p3-openclaw | OpenClawProvider: implement IAgentProvider against OpenClaw REST API | #696 | api | feat/ms23-p3-openclaw-provider | MS23-P2-009 | MS23-P3-002 | — | — | — | 25K | — | Verify barge-in protocol (see PRD open question #3) |
| MS23-P3-002 | not-started | p3-openclaw | OpenClaw session polling / SSE bridge: translate OpenClaw events to AgentMessage | #696 | api | feat/ms23-p3-openclaw-bridge | MS23-P3-001 | MS23-P3-003 | — | — | — | 20K | — | |
| MS23-P3-003 | not-started | p3-openclaw | Provider config UI: register OpenClaw gateway (URL + API token) in Settings | #696 | web | feat/ms23-p3-config-ui | MS23-P3-002 | MS23-P3-004 | — | — | — | 15K | — | |
| MS23-P3-004 | not-started | p3-openclaw | E2E test: OpenClaw provider registered → sessions appear in Mission Control | #696 | api+web | test/ms23-p3 | MS23-P3-003 | MS23-P4-001 | — | — | — | 20K | — | Phase 3 gate |
### Phase 4 — Verification & Release
| id | status | milestone | description | issue | repo | branch | depends_on | blocks | agent | started_at | completed_at | estimate | used | notes |
| ----------- | ----------- | ---------- | --------------------------------------------------------------------------------------- | ----- | ----- | ------ | ----------- | ----------- | ----- | ---------- | ------------ | -------- | ---- | ----- |
| MS23-P4-001 | not-started | p4-release | Full QA: pnpm turbo lint typecheck test — all green | #697 | stack | — | MS23-P3-004 | MS23-P4-002 | — | — | — | 10K | — | |
| MS23-P4-002 | not-started | p4-release | Security review: auth on all new endpoints, audit log integrity, barge-in rate limiting | #697 | api | — | MS23-P4-001 | MS23-P4-003 | — | — | — | 10K | — | |
| MS23-P4-003 | not-started | p4-release | Deploy to production (mosaic.woltje.com), smoke test with live agents | #697 | stack | — | MS23-P4-002 | MS23-P4-004 | — | — | — | 5K | — | |
| MS23-P4-004 | not-started | p4-release | Update ROADMAP.md + CHANGELOG.md, tag v0.0.23 | #697 | stack | — | MS23-P4-003 | — | — | — | — | 3K | — | |
### MS23 Budget Summary
| Phase | Tasks | Estimate |
| ---------------------------- | ------ | --------- |
| Phase 0 — Backend Core | 6 | ~105K |
| Phase 1 — Provider Interface | 6 | ~110K |
| Phase 2 — Mission Control UI | 9 | ~155K |
| Phase 3 — OpenClaw Adapter | 4 | ~80K |
| Phase 4 — Verification | 4 | ~28K |
| **Total** | **29** | **~478K** |
Recommended dispatch: Codex for Phase 2 UI + routine API tasks; Sonnet for complex streaming logic (P0-003, P1-005, P3-002).

21
pnpm-lock.yaml generated
View File

@@ -337,6 +337,9 @@ importers:
'@nestjs/throttler':
specifier: ^6.5.0
version: 6.5.0(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.12)(reflect-metadata@0.2.2)
'@prisma/client':
specifier: ^6.19.2
version: 6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3)
bullmq:
specifier: ^5.67.2
version: 5.67.2
@@ -8005,7 +8008,7 @@ snapshots:
chalk: 5.6.2
commander: 12.1.0
dotenv: 17.2.4
drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
open: 10.2.0
pg: 8.17.2
prettier: 3.8.1
@@ -11345,7 +11348,7 @@ snapshots:
optionalDependencies:
'@prisma/client': 5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
better-sqlite3: 12.6.2
drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
pg: 8.17.2
prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3)
@@ -11370,7 +11373,7 @@ snapshots:
optionalDependencies:
'@prisma/client': 6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3)
better-sqlite3: 12.6.2
drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
drizzle-orm: 0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
next: 16.1.6(@babel/core@7.28.6)(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
pg: 8.17.2
prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3)
@@ -12194,17 +12197,6 @@ snapshots:
dotenv@17.2.4: {}
drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)):
optionalDependencies:
'@opentelemetry/api': 1.9.0
'@prisma/client': 5.22.0(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))
'@types/pg': 8.16.0
better-sqlite3: 12.6.2
kysely: 0.28.10
pg: 8.17.2
postgres: 3.4.8
prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3)
drizzle-orm@0.41.0(@opentelemetry/api@1.9.0)(@prisma/client@6.19.2(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.10)(pg@8.17.2)(postgres@3.4.8)(prisma@6.19.2(magicast@0.3.5)(typescript@5.9.3)):
optionalDependencies:
'@opentelemetry/api': 1.9.0
@@ -12215,7 +12207,6 @@ snapshots:
pg: 8.17.2
postgres: 3.4.8
prisma: 6.19.2(magicast@0.3.5)(typescript@5.9.3)
optional: true
dunder-proto@1.0.1:
dependencies: