feat(M6): Set up orchestrator service foundation
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Add NestJS-based orchestrator service structure for M6-AgentOrchestration. Changes: - Migrate from Express to NestJS architecture - Add health check endpoint module - Add placeholder modules: coordinator, git, killswitch, monitor, queue, spawner, valkey - Update configuration for NestJS - Update lockfile for new dependencies This is foundational work for M6-AgentOrchestration milestone. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
# Orchestrator Configuration
|
||||
ORCHESTRATOR_PORT=3001
|
||||
NODE_ENV=development
|
||||
|
||||
# Valkey
|
||||
VALKEY_HOST=localhost
|
||||
VALKEY_PORT=6379
|
||||
VALKEY_URL=redis://localhost:6379
|
||||
|
||||
# Claude API
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# Mosaic Orchestrator
|
||||
|
||||
Agent orchestration service for Mosaic Stack.
|
||||
Agent orchestration service for Mosaic Stack built with NestJS.
|
||||
|
||||
## Overview
|
||||
|
||||
The Orchestrator is the execution plane of Mosaic Stack, responsible for:
|
||||
|
||||
- Spawning and managing Claude agents
|
||||
- Task queue management (Valkey-backed)
|
||||
- Agent health monitoring and recovery
|
||||
@@ -25,19 +26,36 @@ Monitored via `apps/web/` (Agent Dashboard).
|
||||
# Install dependencies (from monorepo root)
|
||||
pnpm install
|
||||
|
||||
# Run in dev mode
|
||||
# Run in dev mode (watch mode)
|
||||
pnpm --filter @mosaic/orchestrator dev
|
||||
|
||||
# Build
|
||||
pnpm --filter @mosaic/orchestrator build
|
||||
|
||||
# Start production
|
||||
pnpm --filter @mosaic/orchestrator start:prod
|
||||
|
||||
# Test
|
||||
pnpm --filter @mosaic/orchestrator test
|
||||
|
||||
# Generate module (NestJS CLI)
|
||||
cd apps/orchestrator
|
||||
nest generate module <name>
|
||||
nest generate controller <name>
|
||||
nest generate service <name>
|
||||
```
|
||||
|
||||
## NestJS Architecture
|
||||
|
||||
- **Modules:** Feature-based organization (spawner, queue, monitor, etc.)
|
||||
- **Controllers:** HTTP endpoints (health, agents, tasks)
|
||||
- **Services:** Business logic
|
||||
- **Providers:** Dependency injection
|
||||
|
||||
## Configuration
|
||||
|
||||
See `.env.example` for required environment variables.
|
||||
Environment variables loaded via @nestjs/config.
|
||||
See `.env.example` for required vars.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
16
apps/orchestrator/eslint.config.js
Normal file
16
apps/orchestrator/eslint.config.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import nestjsConfig from "@mosaic/config/eslint/nestjs";
|
||||
|
||||
export default [
|
||||
...nestjsConfig,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["dist/**", "node_modules/**", "**/*.test.ts", "**/*.spec.ts"],
|
||||
},
|
||||
];
|
||||
10
apps/orchestrator/nest-cli.json
Normal file
10
apps/orchestrator/nest-cli.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json-schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true,
|
||||
"webpack": false,
|
||||
"tsConfigPath": "tsconfig.json"
|
||||
}
|
||||
}
|
||||
@@ -2,32 +2,47 @@
|
||||
"name": "@mosaic/orchestrator",
|
||||
"version": "0.0.6",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/main.js",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/main.ts",
|
||||
"build": "tsc",
|
||||
"dev": "nest start --watch",
|
||||
"build": "nest build",
|
||||
"start": "node dist/main.js",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main.js",
|
||||
"test": "vitest",
|
||||
"test:watch": "vitest watch",
|
||||
"test:e2e": "vitest run --config tests/integration/vitest.config.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src/",
|
||||
"lint:fix": "eslint src/ --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.31.1",
|
||||
"@anthropic-ai/sdk": "^0.72.1",
|
||||
"@mosaic/shared": "workspace:*",
|
||||
"@mosaic/config": "workspace:*",
|
||||
"fastify": "^5.2.0",
|
||||
"ioredis": "^5.4.2",
|
||||
"@nestjs/common": "^11.1.12",
|
||||
"@nestjs/core": "^11.1.12",
|
||||
"@nestjs/platform-express": "^11.1.12",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/bullmq": "^11.0.4",
|
||||
"bullmq": "^5.67.2",
|
||||
"ioredis": "^5.9.2",
|
||||
"dockerode": "^4.0.2",
|
||||
"simple-git": "^3.27.0",
|
||||
"zod": "^3.24.1"
|
||||
"zod": "^3.24.1",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^11.0.6",
|
||||
"@nestjs/schematics": "^11.0.1",
|
||||
"@nestjs/testing": "^11.1.12",
|
||||
"@types/dockerode": "^3.3.31",
|
||||
"@types/node": "^22.10.5",
|
||||
"tsx": "^4.19.2",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/express": "^5.0.1",
|
||||
"typescript": "^5.8.2",
|
||||
"vitest": "^3.0.8"
|
||||
"vitest": "^4.0.18",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
20
apps/orchestrator/src/api/health/health.controller.ts
Normal file
20
apps/orchestrator/src/api/health/health.controller.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
|
||||
@Controller("health")
|
||||
export class HealthController {
|
||||
@Get()
|
||||
check() {
|
||||
return {
|
||||
status: "ok",
|
||||
service: "orchestrator",
|
||||
version: "0.0.6",
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("ready")
|
||||
ready() {
|
||||
// TODO: Check Valkey connection, Docker daemon
|
||||
return { ready: true };
|
||||
}
|
||||
}
|
||||
7
apps/orchestrator/src/api/health/health.module.ts
Normal file
7
apps/orchestrator/src/api/health/health.module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { HealthController } from "./health.controller";
|
||||
|
||||
@Module({
|
||||
controllers: [HealthController],
|
||||
})
|
||||
export class HealthModule {}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { FastifyPluginAsync } from 'fastify';
|
||||
|
||||
export const healthRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.get('/health', async () => {
|
||||
return {
|
||||
status: 'ok',
|
||||
service: 'orchestrator',
|
||||
version: '0.0.6',
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
});
|
||||
|
||||
fastify.get('/health/ready', async () => {
|
||||
// TODO: Check Valkey connection, Docker daemon
|
||||
return { ready: true };
|
||||
});
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
import Fastify from 'fastify';
|
||||
import { healthRoutes } from './routes/health.routes.js';
|
||||
|
||||
export async function createServer() {
|
||||
const fastify = Fastify({
|
||||
logger: true,
|
||||
});
|
||||
|
||||
// Health check routes
|
||||
await fastify.register(healthRoutes);
|
||||
|
||||
return fastify;
|
||||
}
|
||||
22
apps/orchestrator/src/app.module.ts
Normal file
22
apps/orchestrator/src/app.module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { BullModule } from "@nestjs/bullmq";
|
||||
import { HealthModule } from "./api/health/health.module";
|
||||
import { orchestratorConfig } from "./config/orchestrator.config";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [orchestratorConfig],
|
||||
}),
|
||||
BullModule.forRoot({
|
||||
connection: {
|
||||
host: process.env.VALKEY_HOST ?? "localhost",
|
||||
port: parseInt(process.env.VALKEY_PORT ?? "6379"),
|
||||
},
|
||||
}),
|
||||
HealthModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
26
apps/orchestrator/src/config/orchestrator.config.ts
Normal file
26
apps/orchestrator/src/config/orchestrator.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { registerAs } from "@nestjs/config";
|
||||
|
||||
export const orchestratorConfig = registerAs("orchestrator", () => ({
|
||||
port: parseInt(process.env.ORCHESTRATOR_PORT ?? "3001", 10),
|
||||
valkey: {
|
||||
host: process.env.VALKEY_HOST ?? "localhost",
|
||||
port: parseInt(process.env.VALKEY_PORT ?? "6379", 10),
|
||||
url: process.env.VALKEY_URL ?? "redis://localhost:6379",
|
||||
},
|
||||
claude: {
|
||||
apiKey: process.env.CLAUDE_API_KEY,
|
||||
},
|
||||
docker: {
|
||||
socketPath: process.env.DOCKER_SOCKET ?? "/var/run/docker.sock",
|
||||
},
|
||||
git: {
|
||||
userName: process.env.GIT_USER_NAME ?? "Mosaic Orchestrator",
|
||||
userEmail: process.env.GIT_USER_EMAIL ?? "orchestrator@mosaicstack.dev",
|
||||
},
|
||||
killswitch: {
|
||||
enabled: process.env.KILLSWITCH_ENABLED === "true",
|
||||
},
|
||||
sandbox: {
|
||||
enabled: process.env.SANDBOX_ENABLED === "true",
|
||||
},
|
||||
}));
|
||||
4
apps/orchestrator/src/coordinator/coordinator.module.ts
Normal file
4
apps/orchestrator/src/coordinator/coordinator.module.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({})
|
||||
export class CoordinatorModule {}
|
||||
4
apps/orchestrator/src/git/git.module.ts
Normal file
4
apps/orchestrator/src/git/git.module.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({})
|
||||
export class GitModule {}
|
||||
4
apps/orchestrator/src/killswitch/killswitch.module.ts
Normal file
4
apps/orchestrator/src/killswitch/killswitch.module.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({})
|
||||
export class KillswitchModule {}
|
||||
@@ -1,28 +1,19 @@
|
||||
/**
|
||||
* Mosaic Orchestrator - Agent Orchestration Service
|
||||
*
|
||||
* Execution plane for Mosaic Stack agent coordination.
|
||||
* Spawns, monitors, and manages Claude agents for autonomous work.
|
||||
*/
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { AppModule } from "./app.module";
|
||||
import { Logger } from "@nestjs/common";
|
||||
|
||||
import { createServer } from './api/server.js';
|
||||
|
||||
const PORT = process.env.ORCHESTRATOR_PORT || 3001;
|
||||
const logger = new Logger("Orchestrator");
|
||||
|
||||
async function bootstrap() {
|
||||
console.log('🚀 Starting Mosaic Orchestrator...');
|
||||
|
||||
const server = await createServer();
|
||||
|
||||
await server.listen({
|
||||
port: Number(PORT),
|
||||
host: '0.0.0.0'
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger: ["error", "warn", "log", "debug", "verbose"],
|
||||
});
|
||||
|
||||
console.log(`✅ Orchestrator running on http://0.0.0.0:${PORT}`);
|
||||
const port = process.env.ORCHESTRATOR_PORT ?? 3001;
|
||||
|
||||
await app.listen(Number(port), "0.0.0.0");
|
||||
|
||||
logger.log(`🚀 Orchestrator running on http://0.0.0.0:${String(port)}`);
|
||||
}
|
||||
|
||||
bootstrap().catch((error) => {
|
||||
console.error('Failed to start orchestrator:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
void bootstrap();
|
||||
|
||||
4
apps/orchestrator/src/monitor/monitor.module.ts
Normal file
4
apps/orchestrator/src/monitor/monitor.module.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({})
|
||||
export class MonitorModule {}
|
||||
4
apps/orchestrator/src/queue/queue.module.ts
Normal file
4
apps/orchestrator/src/queue/queue.module.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({})
|
||||
export class QueueModule {}
|
||||
4
apps/orchestrator/src/spawner/spawner.module.ts
Normal file
4
apps/orchestrator/src/spawner/spawner.module.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({})
|
||||
export class SpawnerModule {}
|
||||
4
apps/orchestrator/src/valkey/valkey.module.ts
Normal file
4
apps/orchestrator/src/valkey/valkey.module.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({})
|
||||
export class ValkeyModule {}
|
||||
@@ -1,13 +1,28 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2021",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"baseUrl": "./",
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"lib": ["ES2021"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "tests"]
|
||||
|
||||
Reference in New Issue
Block a user