MILTON Agent Architecture
If you are operating as the central orchestrator (Tech Lead), you must NOT read or modify code directly. Instead, you must use the define_subagent tool to create the following specialized subagents, and then delegate tasks to them using invoke_subagent.
The Tech Lead Workflow
As the central orchestrator, you must follow this exact sequence for every new feature or task:
- Feature Definition & Acceptance Criteria: Do NOT write code or spawn agents immediately. First, collaborate with the user to fully define the requested feature. You must explicitly map out all the different execution paths, edge cases, and establish them as clear Acceptance Criteria.
- Architecture & Contract Planning: Use the
milton-knowledgeMCP server tools (likeget_service_dependencies_map,get_system_architecture_map) to understand which services are involved. Because MILTON uses CQRS with Wolverine, you must explicitly plan and declare the inter-service contracts ([MessageIdentity]records), API endpoints, and schema changes. - Subagent Delegation: Only after contracts are declared and acceptance criteria are locked, use
define_subagentto spin up the appropriate domain experts (e.g.,api-agent,documentgenerator-agent). Hand off the planned contracts and instruct them to implement the feature using Test-Driven Development (TDD).
CRITICAL: The milton-knowledge MCP Server
As the Tech Lead, you must prioritize using the milton-knowledge MCP server tools to understand the system before creating subagents or planning tasks. MILTON is a complex, multi-service architecture mapped into a Vector Database (for semantic documentation) and a Neo4j Graph Database (for structural dependencies).
Do not attempt to guess or brute-force code exploration. “Brute-force code exploration” means using file-system tools (grep_search, list_dir, view_file) as your first step. You MUST query the Vector DB (search_code_context) or Graph DB (get_system_architecture_map) to find where code lives and understand its design pattern BEFORE reading any files.
Vector Database Tools (Semantic Search)
search_code_context: Use this to perform a semantic vector search across system documentation, architecture decisions, and external fact-based knowledge.- System Documentation (Global/Feature Namespaces): Use it to understand how a feature works, or to retrieve precise factual information about the project’s specific tech stack. For example, you can query for:
- Wolverine (messaging, CQRS boundaries, per-producer contracts)
- FastEndpoints (API endpoint structures, handlers)
- Keycloak / YARP (Authentication, BFF patterns, reverse proxy)
- SignalR (Real-time notifications, group routing)
- Entity Framework Core (Migrations, database access patterns)
- External Knowledge (Dedicated Namespaces): Crucially, use it to search for external compliance standards, security frameworks (like the OWASP Top 10), and other fact-based
scraped_dataseeded into the vault. - IMPORTANT: Scraped external knowledge is stored in dedicated namespaces that do NOT appear in the Neo4j graph. You must explicitly set the
agentNamespaceargument to search them (and consider settingincludeGlobal: falseto avoid polluting your results):security- Contains the OWASP Top 10: 2025 standard (Note: ASVS is not currently included).frontend_docs- Contains exhaustive ReactFlow documentation (200+ files detailing nodes, edges, handles, components, and hooks likeuseReactFlow).framework_docs- Contains backend framework documentation specifically for Wolverine (messaging, CQRS) and FastEndpoints.
- Security & Audits (HARD RULE): When asked to evaluate the security, performance, or correctness of a feature, you MUST first query the
securitynamespace to retrieve the project’s baseline standards (OWASP). You must evaluate the code against these retrieved standards, not just your internal knowledge. - General Rule: Whenever evaluating architectural decisions, writing tests, or assessing security, you MUST query the appropriate namespace (
security,frontend_docs,framework_docs) to retrieve explicit project standards and factual documentation.
- System Documentation (Global/Feature Namespaces): Use it to understand how a feature works, or to retrieve precise factual information about the project’s specific tech stack. For example, you can query for:
Neo4j Graph Tools (Structural Architecture)
get_system_architecture_map: Use this to retrieve a zoomed-out visualization of all microservices, API gateways, containers, and their contracts.get_service_dependencies_map: Use this to trace specific service-to-service dependencies, database ownership, and shared volumes.execute_cypher_query: Use this to execute raw Cypher queries against the Neo4j graph. Use it when you need to answer specific structural questions like “Which service subscribes to theNotifyProgressMessagecontract?”
Vector Namespace Discovery
list_all_feature_namespaces: Use this to get a complete list of all features mapped in the system and their corresponding vector namespaces.get_feature_namespace: Use this to query the Neo4j graph for a specific feature and retrieve itsvectorNamespace. You must do this first to get the correct namespace scope before using thesearch_code_contexttool, ensuring your semantic search only searches the relevant feature boundary.
Available Subagents to Define:
- api-agent
- Role: Domain Expert for the main MILTON API service.
- Namespaces:
api,global,documents,projects
- documentgenerator-agent
- Role: Domain Expert for document generation, templating, and lifecycle management.
- Namespaces:
documentgenerator,global,documents,projects
- gitoperations-agent
- Role: Domain Expert for repository cloning, Git operations, and source code analysis.
- Namespaces:
gitoperations,global,projects,clustering
- notification-agent
- Role: Domain Expert for real-time notifications, SignalR hubs, and message delivery.
- Namespaces:
notification,global,api,documents
- clustering-agent
- Role: Domain Expert for code analysis, module detection, and architectural clustering.
- Namespaces:
clustering,global,projects,documents
- knowledge-agent
- Role: Domain Expert for knowledge base operations, embedding generation, graph seeding, and semantic search.
- Namespaces:
knowledge,global,documents,projects
Delegation Workflow for Subagents: Always instruct subagents to follow Test-Driven Development (TDD) using a “Write failing test → Implement → Refactor” workflow. Enforce namespace isolation by instructing subagents to ONLY search for context within their permitted namespaces.
Critical Constraints
- No external CDNs: All assets (CSS, JS, fonts) must be vendored locally — the app may run air-gapped.
- Platform-agnostic paths: Always use
Path.Combine(), never string concatenation with/or\. - No hardcoded secrets: Use
IConfiguration+ user secrets / environment variables. - Offline-first: Do not make calls to external services unless they are explicitly part of the architecture.
- .NET 10 is stable LTS (as of 2026) — do not treat it as preview.
Architecture
MILTON is an AI-powered documentation generation system (SRS, SVD, STD, STR, etc.) with traceability between documents. It follows Vertical Slice Architecture with CQRS using Wolverine for messaging.
Service isolation: each service owns its own contracts and its own data. There is no shared
domain/contracts library — the only shared libraries are MILTON.Platform (technical infrastructure)
and MILTON.ServiceDefaults (Aspire wiring). Services interoperate over RabbitMQ using per-producer
message records bound at the wire by Wolverine [MessageIdentity("…")] (each side declares a
structurally-identical copy). Large payloads travel via an S3 claim-check, not in the message.
Solution Projects
| Project | Role |
|---|---|
MILTON | Main ASP.NET Core 10 API (“Server”) — features, the relational domain, persistence (AppDbContext + EF migrations), and supporting infrastructure (AI presets, PDF/render, encryption, tenant) |
MILTON.ReactClient | React 19 + TanStack frontend; the orval client is generated from the API’s OpenAPI |
MILTON.Platform | Shared technical-infra library: AI client (IAIService/OpenAiCompatibleService), S3 repo-file store + claim-check store, SourceContextBuilder. No domain entities, DTOs, or business contracts |
MILTON.Shared | Removed — folded into the API (DTOs, domain models) and per-producer contracts |
MILTON.BackendCore | Removed — folded into the API |
MILTON.AppHost | .NET Aspire orchestration — defines all services and containers |
MILTON.ServiceDefaults | Common OpenTelemetry, health checks, resilience config |
MILTON.NotificationService | SignalR hub + Wolverine consumer; owns its inbound message + client-event contracts |
MILTON.GitOperations | Wolverine worker for git clone; uploads cloned repos to S3. Owns its Git contracts |
MILTON.DocumentGenerator | Wolverine worker: the document-processing saga + pure generation cores. The processing pipeline is message-only (the API owns document/block state); DocGen also owns its own EF schema (DocGenDbContext) on docgen-db for its forthcoming domain tables, alongside Wolverine’s saga + inbox/outbox |
MILTON.ApiMigrationService | Lightweight worker that applies the API’s EF migrations to milton-db on startup |
MILTON.DocumentGeneratorMigrationService | Lightweight worker that applies DocGenDbContext EF migrations to docgen-db on startup (DocGen waits for it) |
MILTON.ReverseProxy | YARP gateway (TLS termination, unified routing) |
MILTON.Clustering | Python FastAPI microservice (Leiden community detection on code) |
Feature Organization (Vertical Slices)
All API features live in MILTON/Features/. Each feature folder contains its endpoint, command/query record, handler, and any feature-specific logic:
Features/
Projects/ # CreateProject, ListProjects
ProjectConfig/ # GetProjectConfig, UpdateProjectConfig
Templates/ # InstantiateDocument (create a document from a template)
Documents/ # Document/Block/Template endpoints, ProcessBlock, PDF
Generation/ # InstantiateDocumentHandler + the per-block Prepare/Apply handlers,
# the gather helper, and the [MessageIdentity] message + claim-check contracts
Clustering/ # ClusteringEndpoints (proxy to Python service)
Git/ # GitContracts (the API's copy of CloneRepositoriesCommand)
Health/
The block-generation workers (the pure cores + saga) live in MILTON.DocumentGenerator, not the API.
Document Processing Pipeline
The pipeline is message-only: the API owns all document/block state; MILTON.DocumentGenerator
runs the AI work and never touches the database. The round-trip per block:
- (API)
InstantiateDocumentHandlermaterializes the template →Document+Section+ContentBlockrows, then publishesDocumentCreatedEventcarrying the processable block list. - (DocGen)
DocumentProcessingSagaconsumes it and dispatches each block to the API via aPrepareXGenerationCommand. - (API) the
PrepareXhandler gathers everything the worker needs — the block, source-code context (read from S3), and the project’s decrypted LLM presets — writes it to the S3 claim-check store, and replies withXGenerationRequested. - (DocGen) the message-only worker reads the claim-check input, runs the pure core (
IAIServiceonly, no DB), writes the result back to the claim check, and replies withXGenerationCompleted. - (API) the
ApplyXhandler reads the result, writes it to the database, notifies clients, and replies withBlockProcessedEventso the saga advances (and enqueues any spawned blocks). - On completion the saga emits
GeneratePdfCommand(handled by the API) andDocumentGenerationCompletedEvent. Progress/completion/error notifications flow API+DocGen → RabbitMQ →NotificationService→ SignalR.
Real-time Communication
SignalR hub at /hubs/document (hosted in NotificationService, proxied via YARP). Clients call JoinDocumentGroup(documentId). Server pushes: Progress, DocumentCompleted, Error, BlockUpdated, BlocksAdded, SectionsAdded.
Shared Volume for Repositories
API clones repos via IGitService to a shared volume. Path structure: {baseReposPath}/{tenantId}/{projectId}/{repoName}. The Python clustering service accesses repos by path over the same volume — never pass file contents over HTTP.
Key Patterns
FastEndpoints
Every API endpoint inherits from Endpoint<TRequest, TResponse>. Use Send.OkAsync() — not the deprecated SendOkAsync():
public override async Task HandleAsync(MyRequest req, CancellationToken ct)
{
var result = await _bus.InvokeAsync<MyResponse>(command, ct);
await Send.OkAsync(result, cancellation: ct);
}Wolverine Handlers
Prefer static handlers for simple cases. Dependencies are injected as method parameters (not constructors):
public static async Task<MyResponse> Handle(MyCommand cmd, AppDbContext db, CancellationToken ct)
{
// EF Core transaction automatically coordinated by Wolverine
}All messages are persisted to PostgreSQL via durable inbox/outbox — handlers may be retried, so design for idempotency. Do not add manual retry logic.
Creating a New Feature
MILTON/Features/{Name}/— endpoint, command record, handlerMILTON/Shared/DTOs/{Name}/— request/response DTOs (the API’s HTTP contract; the React client is generated from the API’s OpenAPI)MILTON/Shared/Validators/— FluentValidation validators if needed
Inter-service messaging (per-producer contracts)
There is no shared contracts assembly. When a message crosses a service boundary, each side declares
its own structurally-identical record carrying the same Wolverine [MessageIdentity("…")] alias, so
Wolverine routes between the services over RabbitMQ without a shared type. Keep the field shapes
identical (and, for JSON payloads, the [JsonPropertyName] names) or they stop interoperating —
MessageIdentityParityTests and the two-host round-trip tests guard this. Large payloads (source files,
generated content) are written to the S3 claim-check store (IClaimCheckStore in MILTON.Platform)
and the message carries only the claim-check key.
Data per service
The API owns milton-db (relational domain + its Wolverine storage). MILTON.DocumentGenerator owns
docgen-db: Wolverine self-provisions its saga + inbox/outbox tables there at runtime, and DocGen’s own
EF schema (DocGenDbContext) is applied by MILTON.DocumentGeneratorMigrationService on startup (mirroring
how MILTON.ApiMigrationService migrates milton-db). Both databases live on the same Aspire Postgres
server but are separate databases — no cross-service schema sharing.
ContentBlock Types
Blocks are polymorphic; PropertiesJson is typed via block.As<T>() / block.UpdateConfig(model) (from ContentBlockExtensions). Block types: Requirement, TestCase, TestResult, Text, Mermaid, Scanner, TraceMatrix.
LLM Presets
Three built-in presets (Writer, Coder, Analyst) with per-project overrides stored encrypted in ProjectConfig.LlmPresetsJson (jsonb). Use LlmPreset.Resolve() / LlmPreset.ResolveWithOverrides() to get effective config. The API decrypts the preset API keys when gathering a worker’s claim-check payload; the AI client itself (in MILTON.Platform) consumes a technical AiPresetConfig and carries no domain types.
Encryption
Use IEncryptionService (backed by DPAPI) for encrypting sensitive values. Purpose string for tokens: "MILTON.Encryption.Tokens". Never use raw AES or hardcode keys.
Multi-tenancy
ITenantService provides the current tenant ID. TenantInterceptor sets a PostgreSQL session variable on every connection — row-level security is applied automatically.
Python Clustering Service
Communicate via typed HttpClient (registered in DI). Base URL comes from Aspire service discovery (services__clustering__http__0). Never call Python via Process.Start().
Working with MILTON via Aspire
This is tool-neutral guidance for any AI agent or contributor working in this repository.
This repository is set up to use Aspire. Aspire is an orchestrator for the entire application and will take care of configuring dependencies, building, and running the application. The resources that make up the application are defined in AppHost.cs including application code and external dependencies.
General recommendations for working with Aspire
- Before making any changes always run the apphost using
aspire runand inspect the state of resources to make sure you are building from a known state. - Changes to the AppHost.cs file will require a restart of the application to take effect.
- Make changes incrementally and run the aspire application using the
aspire runcommand to validate changes. - Use the Aspire MCP tools to check the status of resources and debug issues.
Running the application
To run the application run the following command:
aspire run
If there is already an instance of the application running it will prompt to stop the existing instance. You only need to restart the application if code in AppHost.cs is changed, but if you experience problems it can be useful to reset everything to the starting state.
Checking resources
To check the status of resources defined in the app model use the list resources tool. This will show you the current state of each resource and if there are any issues. If a resource is not running as expected you can use the execute resource command tool to restart it or perform other actions.
Listing integrations
IMPORTANT! When a user asks you to add a resource to the app model you should first use the list integrations tool to get a list of the current versions of all the available integrations. You should try to use the version of the integration which aligns with the version of the Aspire.AppHost.Sdk. Some integration versions may have a preview suffix. Once you have identified the correct integration you should use the list_docs, search_docs, or get_doc tools to fetch the latest documentation for the integration and follow the links to get additional guidance.
Debugging issues
IMPORTANT! Aspire is designed to capture rich logs and telemetry for all resources defined in the app model. Use the following diagnostic tools when debugging issues with the application before making changes to make sure you are focusing on the right things.
- list structured logs; use this tool to get details about structured logs.
- list console logs; use this tool to get details about console logs.
- list traces; use this tool to get details about traces.
- list trace structured logs; use this tool to get logs related to a trace
Other Aspire MCP tools
- select apphost; use this tool if working with multiple app hosts within a workspace.
- list apphosts; use this tool to get details about active app hosts.
Updating the app host
The user may request that you update the Aspire apphost. You can do this using the aspire update command. This will update the apphost to the latest version and some of the Aspire specific packages in referenced projects, however you may need to manually update other packages in the solution to ensure compatibility. You can consider using the dotnet-outdated with the users consent. To install the dotnet-outdated tool use the following command:
dotnet tool install --global dotnet-outdated-tool
Persistent containers
IMPORTANT! Consider avoiding persistent containers early during development to avoid creating state management issues when restarting the app.
Aspire workload
IMPORTANT! The aspire workload is obsolete. You should never attempt to install or use the Aspire workload.
Official documentation
IMPORTANT! Always prefer official documentation when available. The following sites contain the official documentation for Aspire and related components
- https://aspire.dev
- https://learn.microsoft.com/dotnet/aspire
- https://nuget.org (for specific integration package details)
Commands
# Run the full application (all services)
aspire run
# Build
dotnet build --configuration Release
# Run tests
dotnet test --no-build --configuration Release --verbosity normal
# Run a single test project
dotnet test MILTON.Tests --no-build --verbosity normal
# Add EF Core migration for the API schema (milton-db; run from solution root)
dotnet ef migrations add MigrationName --project MILTON --startup-project MILTON
# Apply API migrations manually (normally done by MILTON.ApiMigrationService on startup)
dotnet ef database update --project MILTON --startup-project MILTON
# Add EF Core migration for the DocumentGenerator schema (docgen-db)
dotnet ef migrations add MigrationName --project MILTON.DocumentGenerator --startup-project MILTON.DocumentGenerator
# Apply DocGen migrations manually (normally done by MILTON.DocumentGeneratorMigrationService on startup)
dotnet ef database update --project MILTON.DocumentGenerator --startup-project MILTON.DocumentGenerator