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:

  1. 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.
  2. Architecture & Contract Planning: Use the milton-knowledge MCP server tools (like get_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.
  3. Subagent Delegation: Only after contracts are declared and acceptance criteria are locked, use define_subagent to 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.

  • 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_data seeded 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 agentNamespace argument to search them (and consider setting includeGlobal: false to 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 like useReactFlow).
      • 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 security namespace 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.

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 the NotifyProgressMessage contract?”

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 its vectorNamespace. You must do this first to get the correct namespace scope before using the search_code_context tool, ensuring your semantic search only searches the relevant feature boundary.

Available Subagents to Define:

  1. api-agent
    • Role: Domain Expert for the main MILTON API service.
    • Namespaces: api, global, documents, projects
  2. documentgenerator-agent
    • Role: Domain Expert for document generation, templating, and lifecycle management.
    • Namespaces: documentgenerator, global, documents, projects
  3. gitoperations-agent
    • Role: Domain Expert for repository cloning, Git operations, and source code analysis.
    • Namespaces: gitoperations, global, projects, clustering
  4. notification-agent
    • Role: Domain Expert for real-time notifications, SignalR hubs, and message delivery.
    • Namespaces: notification, global, api, documents
  5. clustering-agent
    • Role: Domain Expert for code analysis, module detection, and architectural clustering.
    • Namespaces: clustering, global, projects, documents
  6. 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

ProjectRole
MILTONMain ASP.NET Core 10 API (“Server”) — features, the relational domain, persistence (AppDbContext + EF migrations), and supporting infrastructure (AI presets, PDF/render, encryption, tenant)
MILTON.ReactClientReact 19 + TanStack frontend; the orval client is generated from the API’s OpenAPI
MILTON.PlatformShared technical-infra library: AI client (IAIService/OpenAiCompatibleService), S3 repo-file store + claim-check store, SourceContextBuilder. No domain entities, DTOs, or business contracts
MILTON.SharedRemoved — folded into the API (DTOs, domain models) and per-producer contracts
MILTON.BackendCoreRemoved — folded into the API
MILTON.AppHost.NET Aspire orchestration — defines all services and containers
MILTON.ServiceDefaultsCommon OpenTelemetry, health checks, resilience config
MILTON.NotificationServiceSignalR hub + Wolverine consumer; owns its inbound message + client-event contracts
MILTON.GitOperationsWolverine worker for git clone; uploads cloned repos to S3. Owns its Git contracts
MILTON.DocumentGeneratorWolverine 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.ApiMigrationServiceLightweight worker that applies the API’s EF migrations to milton-db on startup
MILTON.DocumentGeneratorMigrationServiceLightweight worker that applies DocGenDbContext EF migrations to docgen-db on startup (DocGen waits for it)
MILTON.ReverseProxyYARP gateway (TLS termination, unified routing)
MILTON.ClusteringPython 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:

  1. (API) InstantiateDocumentHandler materializes the template → Document + Section + ContentBlock rows, then publishes DocumentCreatedEvent carrying the processable block list.
  2. (DocGen) DocumentProcessingSaga consumes it and dispatches each block to the API via a PrepareXGenerationCommand.
  3. (API) the PrepareX handler 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 with XGenerationRequested.
  4. (DocGen) the message-only worker reads the claim-check input, runs the pure core (IAIService only, no DB), writes the result back to the claim check, and replies with XGenerationCompleted.
  5. (API) the ApplyX handler reads the result, writes it to the database, notifies clients, and replies with BlockProcessedEvent so the saga advances (and enqueues any spawned blocks).
  6. On completion the saga emits GeneratePdfCommand (handled by the API) and DocumentGenerationCompletedEvent. 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

  1. MILTON/Features/{Name}/ — endpoint, command record, handler
  2. MILTON/Shared/DTOs/{Name}/ — request/response DTOs (the API’s HTTP contract; the React client is generated from the API’s OpenAPI)
  3. 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

  1. Before making any changes always run the apphost using aspire run and inspect the state of resources to make sure you are building from a known state.
  2. Changes to the AppHost.cs file will require a restart of the application to take effect.
  3. Make changes incrementally and run the aspire application using the aspire run command to validate changes.
  4. 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.

  1. list structured logs; use this tool to get details about structured logs.
  2. list console logs; use this tool to get details about console logs.
  3. list traces; use this tool to get details about traces.
  4. list trace structured logs; use this tool to get logs related to a trace

Other Aspire MCP tools

  1. select apphost; use this tool if working with multiple app hosts within a workspace.
  2. 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

  1. https://aspire.dev
  2. https://learn.microsoft.com/dotnet/aspire
  3. 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