maf-agents
CLAUDE.md0 starsChanged 2 months agoMITView on GitHub
Project memory for maf-agents — agents built with the Microsoft Agent Framework in C# / .NET. It loads automatically every session and governs how Claude Code works in this repository. This file lives at .claude/CLAUDE.md, not the repo root. Detailed standards in .claude/rules/ auto-apply by file path; skills and subagents live under .claude/; settings.json pins model: opus, effortLevel: xhigh, and ten official plugins. The purpose is to build and explore agents with the Microsoft Agent Framework on .NET. Two codebases…
In this repository21 other files it gives its agents.
- angular-developerSkill
- channels-setupSkill
- copilotkit-channelsSkill
- copilotkit-cliSkill
- copilotkitSkill
- csharp-asyncSkill
- csharp-docsSkill
- csharp-xunitSkill
- ef-coreSkill
- github-actions-efficiencySkill
- github-actions-hardeningSkill
- github-actions-runtime-upgrade-conventionsSkill
- inspector-docsSkill
- inspector-workbenchSkill
- intelligence-docsSkill
- microsoft-agent-frameworkSkill
- microsoft-docsSkill
- ngrx-signal-storeSkill
- prdSkill
- setup-slack-channelSkill
- technical-writingSkill
The file
We split this CLAUDE.md into the rules it gives an agent
7 rules. About 0 are common, 4 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 108
Do not re-summarize a plan or diff the user already saw.
- Distinctive: line 113
Write for a senior engineer who knows the language and can read the code.
- Distinctive: line 115
Default to no comment: Add one only where a competent reader would still ask _why_.
- Very specific: line 141
Prefer primary constructors.
- Very specific: line 142
Prefer collection expressions ([], [1, 2, 3], [..
- Distinctive: line 149
When reviewing, make only high-confidence suggestions.
- Very specific: line 180
When reviewing or planning without editing, Read the matching rule directly.
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
CLAUDE.md
Project memory for maf-agents — agents built with the Microsoft Agent Framework in C# / .NET. It loads automatically every session and governs how Claude Code works in this repository.
This file lives at .claude/CLAUDE.md, not the repo root. Detailed standards in .claude/rules/ auto-apply by file path; skills and subagents live under .claude/; settings.json pins model: opus, effortLevel: xhigh, and ten official plugins.
About this repository
The purpose is to build and explore agents with the Microsoft Agent Framework on .NET. Two codebases exist: the .NET solution agents-api/Andes.Agents.slnx, a working ASP.NET Core host for a weather agent, and agents-ui/, its Angular client — read docs/README.md for the engineer-facing reference, CHANGELOG.md for what shipped, and docs/prds/ for what is specified but not yet built.
An earlier shape of this repo — console samples under samples/<NN-category>/ (HelloAgent, ImageAgent), two ADRs, a shared config/appsettings.sample.json and a root maf-agents.slnx — was removed in 40683aa and survives only in history: git show 40683aa^:<path> reads any of it. Its ADRs established two conventions this repository keeps: Foundry is reached through the OpenAI SDK on the resource's /openai/v1/ route with an API key, and stable packages are preferred — with one recorded exception below.
agents-api/ — the API
Six projects, Api → Service → Repository → Entity → Common plus Dto (Dto → Common; Service references Dto, Repository does not), laid out exactly as .claude/rules/api-architecture.md prescribes. That rule is portable and names no repository; this file is where this solution's folder map, deviations and adopted names live — see "How this solution instantiates the architecture rule" below. Api is the composition root and declares a ProjectReference to every project it names — Service, Repository and Dto — rather than reaching them transitively.
- What it does. Entra ID bearer auth on every route (Microsoft.Identity.Web); the agent exposed over A2A (HTTP+JSON and JSON-RPC at
/weather/a2a, card at/.well-known/agent-card.json) and AG-UI (/weather/ui); sessions and messages in Azure Cosmos DB under the hierarchical key[/userId, /sessionId](Entraoid+ A2AcontextId/ AG-UIthreadId);api/conversationsfor a caller's own sessions; SQL Server 2025 through EF Core for an insurance-policy table, the agent catalog and per-session usage summaries; OpenTelemetry → Azure Monitor withgen_ai.*spans; Key Vault when enabled; Scalar over the OpenAPI document; per-caller rate limiting; CORS allow-list; RFC 9457 problem details everywhere. - Model. Two keyed
IChatClientregistrations reach the same Azure AI Foundry resource on its OpenAI-compatible/openai/v1/route, andApi/Options/OpenAIEndpointOptions.csis the shared base that validates that route.AzureOpenAI(required) is the Responses client that drives the agent — deploymentrr-gpt-5.6-luna, stored output disabled so Cosmos is the only conversation state.MicrosoftFoundry(optional) is the Chat Completions client — a blankEndpointregisters nothing rather than failing startup, and nothing consumes it yet. Both go throughOpenAIChatClientFactory.Decorate:FunctionInvokingChatClient → OpenTelemetryChatClient. The agent addsOpenTelemetryAgent → UsageRecordingAgent → ChatClientAgent(Api/Configuration/AgentsConfiguration.cs). Options here expose a computed URI asGetEndpointUri(), a method rather than a property — originally because DataAnnotations reflected over every property and a blank endpoint threw out of the getter before the required-field message could be produced. FluentValidation evaluates only the rules it is given, so that constraint is gone; the method shape simply stayed. - One exception handler and one request log.
Api/ExceptionHandlers/GlobalExceptionHandler.csis the onlyIExceptionHandler: a switch expression maps each exception to its problem+json shape, 500s carry nodetailand no domaintype, and 499 is returned only when the caller actually aborted.Api/Middleware/RequestLoggingMiddleware.cswrites one line per request. Both name the request by its matched route pattern (Api/Observability/RequestDescriptor.cs), so an id in the path never reaches a sink. - Weather data is a deterministic in-process stub (
Service/Weather/WeatherService.cs, owner decision): a fixed gazetteer, accent-insensitive search, seeded pseudo-random readings stable per place and local date. A real provider replaces the implementation behindIWeatherServicewithout touching the tools. SQL Server database — SQL Server 2025 through EF Core 10.
PolicyDbContextmaps five entities, all onBaseEntity: a sequential-GUID clustered key,DateCreated/DateModifiedstamped byAuditTimestampInterceptorunless the caller supplied them, androwversionconcurrency.[Core].[Policy]— a uniquePolicyNumber, a holder index covering the list columns, a(Status, ExpirationDate)index for renewal and expiry sweeps, and check constraints on status names, the coverage window, both amounts and the currency code. Database layer only, by owner decision: no repository, service or endpoint uses it.[Core.Ref].[Agent],[Core.Ref].[Model](deployment, provider name, USD prices per million input, cached-input and output tokens) and[Core.Ref].[AgentModelMapping](activation isDateCreated; a unique index filtered on[DateDeactivated] IS NULLallows one active model per agent). The catalog is owned by the database: the migration that creates it seeds the weather agent throughHasData(keyed byAgentIds) and GPT 5.6 Luna with its mapping as hand-writtenInsertDatathe EF model never sees, the app never writes it, andIAgentCatalogCachereads it once intoIMemoryCachefor 24 hours (Invalidate()for a future writer).[Core].[Session](entitySessionSummary) — one row per session incarnation, unique on(UserId, SessionId, DateCreated): cumulative message and token counts, anEstimatedCostpriced from the catalog as the counts grow, andDateDeletedonce the conversation is deleted.
The connection string (
ConnectionStrings:DefaultConnection) is required,/health/readyincludes it, and startup needs SQL:Api/Startup/AgentCatalogBootstrapper.csrefuses to start unless everyAgentNames.Allhas an active model whose deployment equalsAzureOpenAI:Model.- Packages. Agent Framework core is GA
1.20.0; the hosting packages (Microsoft.Agents.AI.Hosting*,A2A.AspNetCore) exist only in preview and are pinned exactly by owner decision (docs/adr/0001-preview-hosting-packages.md).OpenAIstays at 2.12.0 becauseMicrosoft.Extensions.AI.OpenAI10.9.0 rejects 2.13. The Cosmos SDK's build check demands an explicitNewtonsoft.Jsonpin even though the app serializes with System.Text.Json. EF Core is10.0.12and resolvesMicrosoft.Data.SqlClient6.1, which still bundles the Entra ID providers; SqlClient 7 would needMicrosoft.Data.SqlClient.Extensions.AzureforAuthentication=Active Directory *. - Verified so far:
dotnet build(warnings as errors),dotnet format --verify-no-changes, and a smoke run with fake configuration — liveness, the anonymous card, 401/404 problem+json, OpenAPI/Scalar, forwarded-proto, the auth startup guard. Against a SQL Server 2025 container: the migration applied at startup and viaSqlSchemaMigrator,has-pending-model-changesclean, the unique index and every check constraint rejecting bad rows, audit stamps on sync and async saves, stale-rowversionwrites rejected, 2601/2628 failures leaving no data in logs atInformationor above, theSqlDbstartup guard, and the readiness probe failing inside its 5 s timeout. Against an isolated LocalDB instance (SQL Server 2025): theDateUpdated→DateModifiedrename keeping data, the catalog and session migrations applied bydatabase updateand by the idempotent script run twice withsqlcmd -I, the seed rows, the filtered unique index and check constraints rejecting bad rows; a scratch app covering both usage state keys and a rollback, the one-query catalog cache, every summary merge case (priced deltas, older and equal writes, tombstones, incarnations, 12 concurrent upserts) and the processor (retries, type-only failure logs, drain and prompt shutdown); and the Api refusing to start on a missing mapping or a deployment mismatch. Not yet verified end to end: a real agent turn over AG-UI/A2A and the[Core].[Session]row it produces, the Cosmos writes, the 409 on concurrent turns, App Insights spans, Entra ID sign-in to SQL Server.docs/operations/runbook.mdhas the steps.
Invariants worth knowing before changing the session code
- The session store scopes every lookup by the caller's
oiditself and is registered withwithIsolation: false; the framework's isolation wrapper is deliberately not layered on top (its composed"{key}::{id}"string cannot be split into the two partition-key halves). - Session ids are GUIDs — exactly
DorNform (SessionIdValidator), because[Core].[Session].SessionIdis auniqueidentifier; the hosts issueNwhen a client sends none. A non-GUID id is a 400validation-erroron AG-UI andapi/conversations. The A2A server turns any other exception into a 500 or JSON-RPC −32603, soAgentsConfigurationwraps the store inA2AErrorTranslatingSessionStore, which rethrows it asA2AException(InvalidParams): 400 on HTTP+JSON, −32602 on JSON-RPC. - Message ids are
{sessionId}:{sequence:D8}and the session document is replaced withIfMatchEtag: two concurrent turns on one session end in a 409conversation-busy, never a silent overwrite, and the loser writes nothing. A turn that stored its messages but never saved the session is healed at the next lookup fromMAX(c.sequence)— one cheap query per turn. - Session summaries never touch the turn. After a successful Cosmos save or delete, the store and
SessionServiceenqueue onISessionSummaryChannel(bounded at 10,000; a full channel drops with a warning), andSessionSummaryProcessorwrites in order, pricing from the catalog cache. Cached and reasoning tokens live under their own state key,andes.usage.details, so an older build that rewrites onlyandes.usagecarries them through. The upsert merges cumulative counts: a lower count is an older write and changes nothing, growth addsTokenPrices.CostOfof the delta, and a deletion stampsDateDeletedand never clears it.SessionStoreUnavailableException(EF retries exhausted, aSqlExceptionof severity 17 — out of log, disk, memory or locks — or 20 and above, a timeout) is retried with backoff; any other failure is dropped and logged by exception type names only. AzureAd:ScopesorAzureAd:AppPermissionsmust be configured or startup throws (AzureAdOptions.HasCallerRequirement);AzureAd:AllowAnyAuthenticatedCalleris the explicit opt-out —falseinappsettings.json, so a local run without scopes sets it through user-secrets orAzureAd__AllowAnyAuthenticatedCaller(there is noappsettings.Development.json). Any app in the tenant can obtain a token for the audience — only a scope or app role proves it was granted access.- Nothing a caller supplied may reach a log sink. No bodies, no prompts, no message text, no query values, and never the
oid. Prompt capture on spans is off in every environment;Telemetry:EnableSensitiveDataorOTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=trueis the deliberate opt-in. A caller correlates a failure by thetraceIdon the problem response, which is the Application Insights operation id. Sessionis the domain word;Conversationappears only in the wire route and theconversation-busyproblem type.Chatmeans the model client or wire role. A Service type that persists isPersisted<Thing>— neverCosmos<Thing>, which names a store Service cannot see.Repositoryis provider-first and owns each store end to end. Store-agnostic contracts sit inRepository/<Feature>/Interfaces/(Sessions,Agents); everything Cosmos-specific — client construction, options, serializer, health probe, repositories — sits underRepository/Cosmos/and is registered byAddAndesCosmosPersistence; everything EF Core and SQL Server sits underRepository/Sql/and is registered byAddAndesSqlPersistence.Microsoft.Azure.Cosmosand the EF Core runtime each resolve in exactly one project —Entityreferences onlyMicrosoft.EntityFrameworkCore.Abstractions, for mapping attributes; another store is a sibling folder, not a refactor.- Schema changes are migrations. Change the model and add a migration; the only hand edits are the type-name strings after a CLR rename (
api-architecture.md) and seed rows the EF model must not own — the first model and mapping, written asInsertDataintoCreateSessionReportingTables, which a regeneration drops..editorconfigmarks**/Sql/Migrations/*.csas generated code, becausedotnet efwrites the migration class without an<auto-generated>header.SqlDb:ApplyMigrationsOnStartupis for development; production appliesdotnet ef migrations script --idempotentor a migrations bundle from its pipeline, before the app deploys. Run that script, and any hand edit of[Core.Ref]rows, withsqlcmd -I: DDL and DML on a table with a filtered index needQUOTED_IDENTIFIER ON.SqlPersistenceConfiguration.ConfigureSqlServeris shared withPolicyDbContextDesignTimeFactory, so provider settings change in one place; its compatibility level 170 means SQL Server 2025 or Azure SQL only. - Owner's schema conventions: indexes are named by EF, never by us; strings are
nvarchar; ids, theoidincluded, areuniqueidentifier; timestamps areDateCreatedandDateModified, neverDateUpdated,DateActivatedorDateStarted. Catalog-like data lives in the database, not inappsettings.json. Entity mapping is hybrid: the entity carries its column shape as attributes —[Table(..., Schema = ...)],[Key],[DatabaseGenerated],[Timestamp],[StringLength],[Precision]— and itsIEntityTypeConfiguration<T>carries relationships, indexes, check constraints, conversions and seed data; no length or precision constants. A string is[StringLength]and alwaysnvarchar— never[MaxLength],[Unicode(false)]or a fixed length. Agent ids are constants (AgentIds) and agents areHasData; a model or mapping row never is. - The SQL readiness probe runs
SELECT 1on its own unpooled connection, neverCanConnectAsync, which sits inside the retrying execution strategy and a one-minute login-retry loop. ItsConnect Timeoutcomes from the registration's timeout: SqlClient keeps a cancelled open running until that timeout, and the pooled open the probe first used ignored the token outright — a stopped Docker container held it for the full 15 s default. The price is a full SQL Server login per call on an anonymous, unmetered endpoint; coalescing concurrent probes or restricting who reaches/health/readyis the open follow-up. - A
SqlExceptionmessage quotes data — the duplicate key for 2601/2627, the truncated value for 2628 — and the session summary's key holds theoid. Two guards keep it out of sinks:ConfigureSqlServerlowers EF'sSaveChangesFailed,QueryIterationFailedandCommandErrorevents fromErrortoDebug, andSessionSummaryProcessorlogs a failure by exception type names only. EF sensitive-data logging stays off. - Validation is FluentValidation everywhere, DataAnnotations validation nowhere (the entity mapping attributes above are EF metadata, not validation). Each validated type carries its
AbstractValidator<T>in the same file; options bind with.ValidateWithFluentValidation().ValidateOnStart()against the adapter inCommon/Validation/. The built-inAddValidation()is DataAnnotations-only and is deliberately not registered — it would add a filter to every endpoint, streaming routes included, with no attributes left to act on.[AsParameters]records are checked only whereValidationEndpointFilter.Require<T>()is attached. - Known limits: the A2A task store is in-memory (one instance or sticky sessions); forwarded headers trust any peer that reaches the container;
/health/readyis anonymous and unmetered; the AG-UI host saves nothing for a failed or abandoned stream, so that usage reaches neither store; a session summary queued at a crash or into a full channel is lost until that session's next save.
How this solution instantiates the architecture rule
.claude/rules/api-architecture.md is written in placeholders and names no repository, so it can be copied into any solution unchanged. This section is its instance here.
<Root> = Andes.Agents; <Prefix> = Andes (AddAndesKeyVault, AddAndesTelemetry, AddAndesAuthentication, AddAndesCors, AddAndesRateLimiting, AddAndesProblemDetails, AddAndesExceptionHandling, AddAndesHealthChecks, AddAndesOpenApi, AddAndesAzureCredential, AddAndesCosmosPersistence, AddAndesCosmosHealthCheck, AddAndesSqlPersistence, AddAndesSqlHealthCheck); AddCoreServices registers the clock, the caller context and the prompt loader. <Provider> = Cosmos (sessions and messages) and Sql (policies, the agent catalog and session summaries). The folder-by-folder map is docs/architecture/overview.md.
Sanctioned deviations — where this solution departs from the rule.
- The prompt is an embedded resource, not a shipped
<None>asset:Service/Prompts/weather-agent-instructions.mdis read byPromptTemplateLoaderthrough its logical name inCommon/Constants/PromptNames.cs. - A route may not match its feature. The
Sessionsfeature is served atapi/conversationsbySessionEndpoints; the A2A and AG-UI hosts live inAgentEndpointsatweather/a2aandweather/ui, and the card at/.well-known/agent-card.json. The wire contract wins over the naming rule. Api/Options/OpenAIEndpointOptions.cscarries noSectionName. It is an abstract base, not a section:AzureOpenAIOptionsandMicrosoftFoundryOptionsderive from it and carry the section names, andOpenAIEndpointOptionsValidator<T>is the shared rule set each subclasses so failures name their own section.Problems/ProblemTypes.csis a constant catalog outsideCommon/Constants/. It stays in Api because it is the wire contract of the problem responses; every other catalog obeys the rule.- One exception handler, not one per condition.
GlobalExceptionHandler.csmaps every exception in a single switch expression, so a new domain exception means an arm there and a type inProblemTypes.cs. - The EF context is
PolicyDbContext(the owner's name), not<RootShort>DbContext, and it maps every SQL table, not only policies. - The session-summary queue is
SessionSummaryChannel, not<Subject>Queue: CA1711 is a build error on a public type whose name ends inQueue. [Core].[Session]is mapped bySessionSummary, notSession, becauseISessionRepositoryandSessionDocumentalready name the Cosmos session store.
Design notes — these follow the rule; recorded because the reason is not obvious.
- EF Core lives only in
Repository/Sql/(Agents/,Sessions/,DbContexts/,Configurations/{Policies,Agents,Sessions}/,HealthChecks/,Interceptors/,Migrations/,Provisioning/);Repository/Cosmos/stays on the raw SDK and has noDbContexts/,Configurations/,Interceptors/orMigrations/. Api/Startup/CosmosBootstrapper.cs,SqlBootstrapper.csandAgentCatalogBootstrapper.csare in Api, not the provider folders.Program.csrunsIStartupValidator.Validate()before provisioning or migrating, so nothing external is touched by a process about to fail on its own configuration; a hosted service in Repository would run after that guard. The catalog check runs last, after migrations, and knows which deployment each agent's chat client calls.AuditTimestampInterceptorkeeps timestamps the caller supplied (a defaultDateCreated/DateModifiedon add; aDateModifiedthat differs from its original on update), becauseSessionSummarycopies the Cosmos session's times; every other entity leaves them to the interceptor.TokenPrices.CostOfsits on the Repository record rather than in a Service class: the priced delta depends on the stored counts, soSqlSessionSummaryRepositoryapplies it inside its read-merge-save retry loop.CosmosContainerNamesis interface segregation, not leftover indirection:CosmosContainersandCosmosResourceProvisionerneed three container ids, not the endpoint, key and connection mode.SessionsOptionsValidatoris public while every other options validator is internal, because the composition root in Api registers it across the project boundary.RepositoryreachesCommonthrough the chain (Repository → Entity → Common) rather than declaring it. The composition-root clause applies to Api, where transitive reliance would hide a layering error; below it the declared chain is the contract, andServiceusesCommon.Constantsthe same way.
agents-ui/ — the Angular client
An Angular 22.1 workspace (project agents-ui, prefix app) laid out by .claude/rules/ui-architecture.md, which auto-applies under src/app/. Configured, not yet a product: an app shell, one lazy page (pages/home/) and one root store with its toggle. There is still no dev proxy to agents-api/, but every visitor now signs in automatically through Microsoft Entra ID before anything renders. public/config.json — the shared app registration and the API's base URL, never compiled in — is fetched at runtime; the deployment pipeline overwrites it per environment after the build. src/main.ts short-circuits at /auth: no config fetch, no Angular bootstrap, only msal-browser's redirect bridge relaying the response back to MSAL, because a dedicated page is what msal-browser 5 requires for that hand-back. core/auth/auth-store.ts (AuthStore, an NgRx SignalStore) drives sign-in from provideAppInitializer, which blocks first render, with a sessionStorage-stamped loop breaker so a sign-in that can't complete fails instead of redirecting forever; core/auth/token-refresh-service.ts (TokenRefreshService) then renews the token in the background — a forced refresh at load, a timer inside MSAL's renewal window, and an immediate check on reconnect or when a backgrounded tab turns visible. core/auth/msal.interceptor.ts wraps msal-angular's class-based MsalInterceptor in a functional HttpInterceptorFn, since Angular 22.1's typings say DI-provided interceptors may be phased out. Tokens live in localStorage, so a signed-in tab stays signed in after it's closed and a new tab starts already signed in; there is no sign-out anywhere in the app, by design. See docs/ui/authentication.md.
- Stack. No
zone.js(the v22 default, and TestBed is zoneless too).src/testing/now holds its first fixtures —app-settings.tsand a fakeIPublicClientApplicationinmsal.ts— but still no providers file that wires them all together for a whole-app test. TypeScript 6.0 — the compiler rejects 6.1 and 7 — withpathsaliases@pages/*…@testing/*and nobaseUrl, which TS 6 deprecates. Vitest 4 throughng test(the builder rejects Vitest 5) on jsdom.@ngrx/signals22. npm; Node 22.22+ or 24.15+. - Bootstrap is compiled from Sass with the brand palette.
src/styles.scssloads it with@use 'bootstrap/scss/bootstrap' with (...): Summit Navyprimary, Slatesecondary, Glacier Blueinfo, Andes Greensuccess, Ink on Snow in light, Snow on Ink with Summit Navy surfaces in dark. Components, focus rings and button text all derive from those values; only Bootstrap Icons stays precompiled CSS inangular.json. Angular compiles Sass withquietDeps, so Bootstrap's internal deprecations stay silent, and@use … withadds none (a@importin our own file would). Components generate without a stylesheet ("style": "none"). The initial budget warns at 900 kB (raised from 750 kB) and errors at 1 MB, unchanged: Bootstrap and Icons CSS are ~315 kB raw, and@azure/msal-browser— ~280 kB minified, tree-shakes poorly, and must ship in the initial chunk because sign-in blocks first render — pushed the initial total to 887.57 kB raw. - The dark theme re-points primary at Glacier Blue. Bootstrap bakes
$primaryinto components once for both themes and Summit Navy is 1.27:1 on Ink, so the[data-bs-theme='dark']block instyles.scssoverrides primary buttons, focus rings, checked inputs, nav pills, dropdown and list-group active items, pagination and progress. A component that bakes in primary and is missing from that block stays navy in dark mode — add it when the app first uses it. Dark muted text is Snow at 75% (Slate is 3.17:1 on Ink), and dark.btn-outline-secondaryuses the same tone because Slate is 2.8:1 on the dark navbar. Glacier Blue and Andes Green are 2.6:1 and 2.5:1 as text on Snow: use them as fills and icons, andtext-info-emphasis/text-success-emphasisfor light-theme text. - Styling is Bootstrap-first, mobile-first and palette-bound — a standing rule in
.claude/rules/ui-architecture.md§Styling and layout, which also loads forsrc/styles.scssandsrc/styles/**: Bootstrap 5.3 components and utilities before custom CSS, standalone ng-bootstrap directives for interactive widgets, screens designed at 360 px and checked at 360, 768 and 1280 px in both themes, colours only through the theme the palette feeds, CSS that only a lazy page needs in a non-injected style bundle, and a third-party design system re-themed onto--bs-*tokens rather than adopted. The planned CopilotKit chat is specified that way indocs/prds/copilotkit/. - ng-bootstrap 21 was installed with npm, not
ng add. Its schematic registersNgbModuleat the root and writes a Bootstrap SCSS@importthat Sass 1.101 flags as deprecated; components import standaloneNgb*directives instead.@angular/localizecame through its ownng addbecause ng-bootstrap needs$localize— it is the only polyfill. - OnPush is Angular 22's default, so components omit
changeDetection;prefer-on-push-component-change-detectionruns withallowExplicitOnPush: falseand rejects an opt-out and a redundant OnPush alike. ThemeStore(state/theme-store.ts) holds light/dark, applies it asdata-bs-themeon<html>throughwatchState, and persists only an explicit choice (andes.themeinlocalStorage), so a visitor who never toggles follows the OS.shared/components/theme-toggle/binds to it and carries anngbTooltip.eslint.config.jsenforces the rule's layer table: oneno-restricted-importsblock per folder, specs exempt, each banned folder as its alias and as**/app/<folder>/*, with per-feature blocks generated from the top-level folders ofsrc/app/componentsandsrc/app/pages. It works around three traps. Flat config replaces a rule's options when blocks overlap, so every block repeats theFormsModule/ReactiveFormsModuleban. An exception negates the directory (!@components/<f>), because the matcher cannot re-include a child of an excluded directory. A relative import with noapp/segment (../../state/x) is not caught, so cross-layer imports must use the alias. NgRx'ssignalsTypeCheckedrules are added by hand and scoped to**/*.ts: itsng addskips any config withouttseslint.config, and unscoped its parser would replace the template parser on.html.- Prettier sorts imports in the rule's order (
@ianvs/prettier-plugin-sort-imports; third-party packages sit right after Angular). npm run check:initial-chunkbuilds with--stats-jsonand fails when anything underpages/,components/orstate/<feature>/is statically reachable frommainor the polyfills, or when a file inpageRoots(scripts/check-initial-chunk.mjs) is not a lazy entry point. A new or moved page means updatingpageRoots.- Verified so far:
ng buildwith no warnings at the 900 kB budget (initial 887.57 kB raw),ng test(43 tests),ng lint,prettier --check, and the initial-chunk gate passing; also, from before sign-in was added, the gate failing on a statically imported page, every layer ban firing — and not firing — on deliberate violations, and headless Edge againstng serve: nozone.jsand no console errors, the palette in both themes (computed colors of buttons, links, muted text, focus rings, checked inputs, pills, list-group, pagination, progress and alerts, every text pair at AA or better), the icon font, the lazy home page, the tooltip (its text updating while open), the OS scheme followed while nothing is stored, and a toggle persisting across a reload. Headless Edge againstng servefor sign-in:config.jsonfetched before the Entraauthorizenavigation, which carried the configured tenant andclient_id,redirect_uri=<origin>/auth,<client>/.defaultwithoffline_access, and PKCE S256, while only "Signing in…" was on screen and nozone.jsloaded; a substitutedconfig.jsonreaching that URL without a rebuild; a 404 and a missingAzureAd.ClientIdeach showing the configuration error with no Entra navigation; and/authwithout a response never bootstrapping, fetching no config and falling back to/. Not yet verified: a real Entra ID sign-in through a browser, token claims (aud,ver,scp), whether a second silent.defaultcall is served from cache, and a browser-driven call toagents-api/.
Build and tooling reality
Directory.Build.propsandDirectory.Packages.propssit beside the.slnx. Central package management is on — aPackageReferencenever carries aVersion; add new versions to the props file.TreatWarningsAsErrors,EnforceCodeStyleInBuild,AnalysisLevel latest-recommendedandGenerateDocumentationFileare all on, so.editorconfig(file-scoped namespaces,_camelCasefields, IDE0005 unused usings, formatting) and CS1591 missing-XML-doc are build errors. Every public type and member needs a<summary>; positional records need either no<param>tags or all of them. The recommended analyzers include CA1711 (no public…Queue/…Collectionnames), CA1816 and CA2215 (aDisposeoverride must callGC.SuppressFinalizeand the base) and CA1859 (concrete types for private fields and locals).- Evaluation-only APIs (
OPENAI001for the Responses client,MAAI001for the stored-output-disabled adapter) are suppressed with a call-site#pragma, never project-wide. - No
global.json; SDK 10.0.401 builds it..gitattributesforces* text=auto eol=lf. dotnet-tools.jsonat the repo root pinsdotnet-ef10.0.12 to match the EF packages. Rundotnet tool restore, thendotnet tool run dotnet-ef …: a globaldotnet-efon PATH may be older. Repository is both target and startup project, so the tools never boot the Api host.- No CI, and no .NET tests.
dotnet buildanddotnet format --verify-no-changesare the API's local gates; the smoke procedure in the runbook is manual.agents-ui/has its own (see Common commands). - On Windows a test server started from Git Bash is stopped by port (
netstat -ano | grep ":<port> "thentaskkill //F //PID <pid>);pkillandtaskkill //IMleave it running and lock the DLL for the next build.
Secrets
Nothing sensitive is committed. appsettings.json ships every key with secrets blank; local values go into dotnet user-secrets (the Api project has a UserSecretsId) or environment variables (Foundry__ApiKey, CosmosDb__Key, ConnectionStrings__DefaultConnection, …). With KeyVault:Enabled, secrets named with -- (Foundry--ApiKey, ConnectionStrings--DefaultConnection) layer on top through DefaultAzureCredential. The Foundry API key has no managed-identity equivalent, which is the one accepted carve-out from the prefer-DefaultAzureCredential standard; Cosmos uses the shared credential whenever CosmosDb:Key is blank. SQL Server's connection string follows the Azure shape — ConnectionStrings:DefaultConnection, not a key inside SqlDb — so an App Service connection string named DefaultConnection lands on it. It signs in however that string says: Windows authentication for the LocalDB andes-agents default in appsettings.json, SQL authentication for the Docker container, Authentication=Active Directory Managed Identity in Azure.
Communication & comments (always)
Responses
- Lead with the answer or the change. No preamble, no filler, no restating the request.
- Do not re-summarize a plan or diff the user already saw; report only what changed or went wrong.
- Match length to substance — a one-line answer is a complete answer.
Code comments
Write for a senior engineer who knows the language and can read the code. A comment supplies what the code cannot — nothing else.
- Default to no comment. Add one only where a competent reader would still ask why. No per-type or per-member quota.
- One or two lines. Three needs a reason; past six the content belongs in
docs/with a one-line pointer from the code. - Never reference PRD story ids, epics, or design frames. Traceability belongs in the PR description and
docs/; in source it goes stale and says nothing about the code in front of the reader. Test names describe behaviour, not story ids. - Never explain the language, framework, or API — not what a primary constructor is, not how
IAsyncEnumerableworks, not whatnameofreturns. - Never narrate. No "added X", "now uses Y". No restating the statement below it.
- Do write: why the non-obvious choice beat the obvious one, an invariant a caller must hold, a workaround with its link or issue, a spec or vendor constraint. State the cause once — not the argument, the alternatives weighed, or the history.
- Compress rather than delete. A real why buried in a paragraph becomes one sentence; it does not disappear.
// Bad — narrates, teaches the platform, cites a story
/// <summary>
/// US-118 asks the tool to check the image before the call. A primary constructor captures
/// the client, and IAsyncEnumerable lets the caller stream results as they arrive. Added
/// local validation here so we fail fast instead of round-tripping to the service.
/// </summary>
// Good — the one thing the code cannot say
/// <remarks>Checked locally: the service rejects masks over 4 MB with an opaque 400.</remarks>C# coding standards (always)
Full standards live in .claude/rules/csharp.md (auto-applies to **/*.cs), and .editorconfig encodes them for dotnet format and, through EnforceCodeStyleInBuild, for the build. The always-on core:
- Target the latest C# version (currently C# 14); prefer pattern matching, switch expressions, and
nameof(...). - File-scoped namespaces everywhere —
.editorconfigsets this aterrorseverity and every file already complies. - Prefer primary constructors; capture each injected dependency into a
private readonly_camelCasefield and use the field in method bodies. - Prefer collection expressions (
[],[1, 2, 3],[.. items]) overnew List<T>(),new T[] { }, orArray.Empty<T>(). - PascalCase for types, methods, and public members;
_camelCaseprivate fields (constants included); camelCase locals and parameters;I-prefixed interfaces. - Declare variables non-nullable; validate
nullat entry points only; useis null/is not null— never== null/!= null. - Suffix async methods with
Async; never block with.Result,.Wait(), or.GetAwaiter().GetResult(); noasync voidoutside event handlers; flow aCancellationTokenthrough long-running operations (see thecsharp-asyncskill).ConfigureAwait(false)is used inServiceandRepositoryand omitted inApi— follow the surrounding project. - Never log PII or secrets. The
oidis a personal identifier: it goes into partition keys and the reporting store, never into log messages or problem bodies. - XML docs — a one-sentence
<summary>on public API surface; interfaces carry the docs and implementations use/// <inheritdoc />.<param>/<returns>only where they add what the signature does not.<remarks>is for a caveat a caller must know, two sentences at most — not rationale, not history.<example>/<code>only where correct usage is genuinely non-obvious.internaland test types are not API surface: comment them only where a why exists. This overrides thecsharp-docsskill wherever the two disagree — that skill describes .NET's framework-reference house style, which is not this repository's. - Member order in services and implementations: interface or override methods, other public instance methods, then
#region Public Static Methods,#region Private Methods,#region Loggers(each only when non-empty); nested types last (csharp.md). - When reviewing, make only high-confidence suggestions; comment on why a non-obvious design decision was made.
Tests. No .NET tests exist yet. When they do: xUnit v3 under agents-api/tests/, named MethodName_Scenario_ExpectedBehavior, Arrange-Act-Assert structure but no // Arrange / // Act / // Assert comments, plain xUnit Assert (no FluentAssertions — v8+ is commercially licensed), isolation with NSubstitute and never Moq, and integration tests against Testcontainers (a local test instance only where no image exists). Agent code that calls a live model is not unit-tested. The csharp-xunit skill suggests Moq and a fluent assertion library; csharp.md overrides it.
LINQ. Method syntax only — .Where(...).Join(...).Select(...), never query syntax (from … in … select).
Validation. FluentValidation only — no System.ComponentModel.DataAnnotations validation anywhere; EF mapping attributes on entities are not validation. Each validated type declares its AbstractValidator<T> in the same file, which is the one sanctioned exception to file-name-equals-type-name.
Skills
The roster (names + descriptions) is always in context; these are the routing notes it lacks.
microsoft-agent-frameworkis the primary skill for this repository. The framework is in public preview and the Learn API reference lags the packages (it still showsAgentThread,MapAGUI,UseClaimsBasedSessionIsolation); ground advice in themainsource and conceptual docs throughmicrosoft-docs, and trust the compiler over both.csharp-async,csharp-docs,csharp-xunitandef-coreare preloaded by thecsharp-code-reviewersubagent; each is invokable on its own.ef-coreapplies toRepository/Sql/only — Cosmos persistence is the raw SDK.prdis preloaded byprd-generator;technical-writing(document-type templates) byse-technical-writer.copilotkitandcopilotkit-cliapply once CopilotKit lands inagents-ui/; until then they are the way to re-verifydocs/prds/copilotkit/. The channels, Slack, Inspector and Intelligence skills describe CopilotKit's runtime and hosted tier, which this repository's design does not use.- The three
github-actions-*skills are the lanes preloaded bygithub-actions-reviewer. There is no.github/directory in this repository, so all four are idle until someone adds workflows.
Rules — .claude/rules/
Six files, auto-loaded when you edit a matching path; the globs live in each rule's frontmatter under a paths: list, and none carry a description:.
| Rule | paths: glob | Applies here? |
csharp.md | **/*.cs | Yes — the standards above. |
api-architecture.md | **/*.cs, **/*.csproj | Yes — where every file goes and what it is called. Portable and repo-agnostic; this solution's instance of it is in §"How this solution instantiates the architecture rule". |
ui-architecture.md | **/src/app/**, **/src/testing/**, **/src/styles.scss, **/src/styles/** | Yes — agents-ui/: where every file goes, what it is called, which way imports run, and how screens are styled (Bootstrap-first, mobile-first, palette tokens). eslint.config.js and scripts/check-initial-chunk.mjs enforce the layout; see §"agents-ui/ — the Angular client". |
aspnet-rest-apis.md | **/*.cs | Loads on every C# edit; relevant to Andes.Agents.Api. |
azure-functions-csharp.md | **/*.cs | Loads on every C# edit, but there are no Azure Functions here. Ignore it. |
terraform.md | **/*.tf | No .tf files exist; never matches. |
When reviewing or planning without editing, Read the matching rule directly.
MCP servers — see @.mcp.json
.claude/settings.json sets enableAllProjectMcpServers: true, so the servers in @.mcp.json are available. Four are defined:
microsoft-learn(http) — ground version-specific .NET, Azure and Agent Framework answers in official docs (microsoft_docs_search→microsoft_code_sample_search→microsoft_docs_fetch) instead of memory.context7(npx) — docs outside learn.microsoft.com; resolve the library id first, then query.angular-cli(npx,@angular/cli@22 mcp --read-only) — the Angular CLI's own server (list_projects,get_best_practices,search_documentation), pinned to the majoragents-ui/runs; thengrx-signal-storeskill sends non-state Angular questions here.copilotkit-docs(http,https://mcp.copilotkit.ai/mcp) — CopilotKit's documentation server (search-docs,search-code,search-ag-ui-docs,search-ag-ui-code), which thecopilotkitskills expect. CopilotKit and AG-UI change weekly: re-verify through it before relying on anything indocs/prds/copilotkit/sources.md.
settings.json also lists terraform under enabledMcpjsonServers, but .mcp.json does not define it, so that entry does nothing.
Trust gate: a checked-in
.claude/settings.jsoncannot approve its own repo's MCP servers while the folder is untrusted — the key is ignored and servers sit at "Pending approval" until the workspace trust dialog is accepted. To auto-approve regardless, add a name-based list to your user-level~/.claude/settings.json:"enabledMcpjsonServers": ["microsoft-learn", "context7", "angular-cli", "copilotkit-docs"]. If a server shows Rejected, a stale per-project choice is cached — runclaude mcp reset-project-choicesin this repo.
Delegation
Four subagents, all pinned to extra-high reasoning effort; csharp-code-reviewer and github-actions-reviewer run on opus, the other two on sonnet. Tools and preloaded skills live in each agent's frontmatter. Reviewer loops are capped: apply Critical/High findings, re-review only the changed files, at most two rounds, then surface anything still open to the user.
- After implementing or modifying C# code, delegate a quality review to
csharp-code-reviewer. It reports findings; it does not edit files. - After a feature is implemented and the reviewer verdict passes, delegate to
se-technical-writerto update the Markdown underdocs/and add the entry to the rootCHANGELOG.md— Keep a Changelog format, one## [Unreleased]section, one reader-facing entry per PR in the matching subsection.docs/followsdocs/README.md's index:architecture/,conversations/,agent/,ui/,operations/,adr/,prds/. github-actions-reviewerhas nothing to review until a.github/workflows/tree exists. If workflows are added, route to it.- A PRD the owner explicitly requests is checked in under
docs/prds/<topic>/— todaydocs/prds/copilotkit/(the planned chat, with learning docs, decisions, designs and verification) anddocs/prds/a2ui/.prd-generatorand theprdskill write them; nothing routes to them by default. A PRD is pre-implementation: it gets noCHANGELOG.mdentry of its own, and its decisions become ADRs underdocs/adr/only when the work is built.
Common commands
# from the repo root
dotnet build agents-api/Andes.Agents.slnx
dotnet format agents-api/Andes.Agents.slnx --verify-no-changes # read-only check
dotnet run --project agents-api/Andes.Agents.Api # needs Foundry, Cosmos, SQL Server and AzureAd configured — see docs/operations/runbook.md
# EF Core migrations for PolicyDbContext (Repository is target and startup project)
dotnet tool restore
dotnet tool run dotnet-ef migrations add <Verb><Subject> --project agents-api/Andes.Agents.Repository --startup-project agents-api/Andes.Agents.Repository --context PolicyDbContext --output-dir Sql/Migrations
dotnet tool run dotnet-ef migrations has-pending-model-changes --project agents-api/Andes.Agents.Repository --startup-project agents-api/Andes.Agents.Repository --context PolicyDbContext
dotnet tool run dotnet-ef migrations script --idempotent --project agents-api/Andes.Agents.Repository --startup-project agents-api/Andes.Agents.Repository --context PolicyDbContext -o migrations.sql
sqlcmd -S <server> -d <database> -I -b -i migrations.sql # -I: the filtered index needs QUOTED_IDENTIFIER ON
# agents-ui (Angular), from agents-ui/
npm ci
npm start # ng serve on http://localhost:4200
npm test -- --watch=false # Vitest, single run
npm run lint # angular-eslint, NgRx rules and the layer bans
npm run format:check # Prettier, including import order
npm run check:initial-chunk # production build, then the initial-chunk gateBoth the build and the format check pass on the current tree. There is no dotnet test — no test project exists. There is no root solution, so every command names its path explicitly.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

