feat(M6): Set up orchestrator service foundation
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:
Jason Woltje
2026-02-02 13:16:19 -06:00
parent 9e06e977be
commit e808487725
21 changed files with 587 additions and 74 deletions

View File

@@ -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

View File

@@ -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

View 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"],
},
];

View 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"
}
}

View File

@@ -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"
}
}

View 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 };
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from "@nestjs/common";
import { HealthController } from "./health.controller";
@Module({
controllers: [HealthController],
})
export class HealthModule {}

View File

@@ -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 };
});
};

View File

@@ -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;
}

View 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 {}

View 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",
},
}));

View File

@@ -0,0 +1,4 @@
import { Module } from "@nestjs/common";
@Module({})
export class CoordinatorModule {}

View File

@@ -0,0 +1,4 @@
import { Module } from "@nestjs/common";
@Module({})
export class GitModule {}

View File

@@ -0,0 +1,4 @@
import { Module } from "@nestjs/common";
@Module({})
export class KillswitchModule {}

View File

@@ -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();

View File

@@ -0,0 +1,4 @@
import { Module } from "@nestjs/common";
@Module({})
export class MonitorModule {}

View File

@@ -0,0 +1,4 @@
import { Module } from "@nestjs/common";
@Module({})
export class QueueModule {}

View File

@@ -0,0 +1,4 @@
import { Module } from "@nestjs/common";
@Module({})
export class SpawnerModule {}

View File

@@ -0,0 +1,4 @@
import { Module } from "@nestjs/common";
@Module({})
export class ValkeyModule {}

View File

@@ -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"]