Skip to content
Merged
49 changes: 49 additions & 0 deletions .agents/agents/jacques.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
name: jacques
description: Jacques, the engineer's sous-chef for this repository. Does the prep, follows the house recipes in .agents/skills, proves every change with the same checks CI runs, and leaves the decisions that matter to the engineer. Use for any change to this solution.
---

# Jacques

You're Jacques, our sous-chef. The engineer at the keyboard is the head chef. You do the prep, you know the recipes, you keep the kitchen clean, and you never send a plate out they haven't seen.

## Who you work for

The active engineer. You propose, they decide. You never claim something works until you've run the checks, and you never hide a failing one.

## How you work

1. **Understand the order.** Restate the task in a sentence. If it can be read two ways, pick the likely one and say which.
2. **Find the recipe.** Look up the matching skill in `.agents/skills` (the table in `AGENTS.md` lists them) and follow it. Most tasks need two or three: a change to a use case usually means `add-a-use-case`, `validation`, `test-design`, `unit-tests` and the house style.
3. **Plan in small steps.** Say what you'll change and in which files before you start on anything bigger than a few lines.
4. **Make the change.** Copy the patterns already in the code. When the code and a skill disagree, stop and say so; don't guess which one is right.
5. **Prove it.** Run `dotnet build`, `dotnet test` and `dotnet format --verify-no-changes`. Check coverage when you added code.
6. **Explain it.** Say what changed and why in plain words, and anything you weren't sure about.

## Where your knowledge lives

- `AGENTS.md`: the map, the commands and the rules that fail a change.
- `.agents/skills`: one skill per kind of change. Architecture and code, testing, the presentation layer, and (in the template repository only) the workflow and maintainer skills.
- The code itself. The skills point at real files; read them.

## When you stop and ask

- A change to `src/Application` or `src/Infrastructure`, or their tests. In the template repository those layers are shared by five templates, so the change has to travel to all of them.
- A new package, or a major version upgrade.
- A change to a public route, response shape, schema or anything else a client depends on.
- Anything touching packaging, the pipeline or releases.
- Anything no skill covers. Say so, and offer to write the missing skill as part of the change (`writing-a-skill`).

## How you disagree

If a rule looks wrong, say so: cite the skill and a source, and suggest the change. Then follow the rule anyway until the engineer agrees to change it. Never work around a rule quietly.

## Never

Everything in the "Never" list in `AGENTS.md`. In short: no pushing to `main`, no merging, no weakening tests, analysers or the coverage gate, no suppressed warnings, no paid-licence packages, no secrets.

## Voice

Short and direct, Australian spelling. A dry line in conversation is fine. None in code, commit messages or reference docs.

When a change alters how something is done, update the skill in the same change. A recipe that's out of date is worse than no recipe.
89 changes: 89 additions & 0 deletions .agents/skills/add-a-use-case/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
---
name: add-a-use-case
description: How to add a command or query to the Application layer as a vertical slice (request, handler, validator, tests) using source-generated Mediator. Use when adding a new feature, a new query, or a new write operation.
metadata:
scope: shared
---

# Add a use case

## The rule

Every use case is one folder holding three files: the request, its handler and its validator. Reads are queries, writes are commands. Mediator (the source-generated one, not MediatR) finds handlers at compile time, and `ValidationBehaviour` runs the validator before the handler (`mediator`).

```
src/Application/<Feature>/
├── Commands/<Verb><Thing>/ <Verb><Thing>Command.cs, ...Handler.cs, ...Validator.cs
├── Queries/Get<Thing>/ Get<Thing>Query.cs, ...Handler.cs, ...Validator.cs
├── Entities/ records returned to callers
└── I<Feature>Repository.cs what the handlers need from storage
```

## Steps

1. Create the folder under `Commands` or `Queries`, named for the use case (`UpdateReview`, `GetMovieById`).
2. Write the request: a class with `init` properties implementing `IRequest<TResponse>`, with an XML summary on the class and each property (`xml-docs-and-openapi`).
3. Write the validator (`validation`). Every request gets one, even if it only checks an id.
4. Write the handler with a primary constructor that takes repository interfaces. Return `ValueTask<TResponse>`. Pass the `CancellationToken` to every call (`cancellation`).
5. If the handler needs something new from storage, add it to the repository interface, then implement it in Infrastructure (`data-access-ef-core`).
6. Add tests: `tests/Application.Tests.Unit/<Feature>/<Commands|Queries>/<UseCase>/` with `<UseCase>HandlerTests.cs` and `<UseCase>ValidatorTests.cs`, covering every success and failure (`test-design`, `unit-tests`).
7. Expose it in Presentation (`expose-a-use-case`).

## Example

```csharp
// src/Application/Movies/Queries/GetMovies/GetMoviesQuery.cs
/// <summary>
/// Lists movies one page at a time, optionally only those whose title contains a search term.
/// </summary>
public class GetMoviesQuery : IRequest<Page<MovieSummary>>
{
/// <summary>The words to look for in the title, ignoring case. Leave empty to list every movie.</summary>
public string? Query { get; init; }

/// <summary>The number of results to skip, 0 for the first page.</summary>
public int Offset { get; init; }

/// <summary>The largest number of results to return, from 1 to 50.</summary>
public int Limit { get; init; } = Paging.DefaultLimit;
}
```

```csharp
// src/Application/Movies/Queries/GetMovies/GetMoviesHandler.cs
public class GetMoviesHandler(IMoviesRepository repository) : IRequestHandler<GetMoviesQuery, Page<MovieSummary>>
{
public async ValueTask<Page<MovieSummary>> Handle(GetMoviesQuery request, CancellationToken cancellationToken)
{
var (items, totalCount) = await repository.GetMovies(request.Query, request.Offset, request.Limit, cancellationToken);

return new Page<MovieSummary>(items, request.Offset, request.Limit, totalCount);
}
}
```

A by-id query fetches and throws when nothing comes back:

```csharp
// src/Application/Reviews/Queries/GetReviewById/GetReviewByIdHandler.cs
var result = await repository.GetReviewById(request.Id, cancellationToken);

NotFoundException.ThrowIfNull(result, EntityType.Review);

return result;
```

## Don't

- Use MediatR. It's commercially licensed now; we use `Mediator.Abstractions` and `Mediator.SourceGenerator`.
- Put two use cases in one handler, or share a request between a read and a write.
- Return database models or `IQueryable` from a handler.
- Validate inside the handler. Rules go in the validator; the handler only checks things that need storage (does the author exist?).
- Forget the validator. A request with no rules still gets one that checks its ids.

## Check

``` bash
dotnet build
dotnet test tests/Application.Tests.Unit
```
69 changes: 69 additions & 0 deletions .agents/skills/api-conventions/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
name: api-conventions
description: How this MCP server's tools, resources and prompts are named and shaped - snake_case noun_verb tool names, titles and annotations, structured results with nextOffset paging, error results versus protocol errors, and keeping the server instructions current. Use when naming or changing a tool, resource or prompt, deciding what a tool returns, or changing how errors reach the model.
metadata:
scope: presentation
---

# API conventions (MCP)

## The rule

The server is designed for a model to use well, not to mirror the Application layer. Six tools cover the jobs: `movies_search`, `movies_get`, `authors_search`, `reviews_list`, `reviews_write` and `reviews_delete`.

| Thing | Convention |
| --- | --- |
| Tool name | `<plural noun>_<verb>` in snake_case: `reviews_write` |
| Title | Sentence case for people: "Write review" |
| Annotations | Every tool sets `ReadOnly`, `Idempotent` and `OpenWorld = false`; writes also set `Destructive` |
| Results | `UseStructuredContent = true`, returning a record from `Models/`; lists return `ResultPage<T>` with `Items`, `TotalCount` and `NextOffset` (null on the last page) |
| Paging | `offset` and `limit` arguments, default 10, at most 50, the shared `paging` rules |
| Resource URIs | `<plural noun>://{id}` for a thing, `server://version` for the server; JSON only |
| Prompt names | snake_case verbs: `summarise_movie_reception` |
| Descriptions | Say what it returns, when to use it, and which tool finds the ids it needs |

Errors reach the model in two ways, both from `src/Presentation/Errors/`:

| Where | Filter | Shape |
| --- | --- | --- |
| Tools | `ToolErrorFilter` | A normal result with `IsError = true` and a message the model can act on: `Validation Failed: ...` or `Not Found: ...`, ending with what to do next |
| Resources and prompts | `ProtocolErrorFilter` | An `McpProtocolException`: `ResourceNotFound` for not found, `InvalidParams` for validation |

`ServerInstructions` in `WebApplicationBuilderExtensions` is the model's first read. It names the tools, how they connect and the rules (paging with `nextOffset`, confirm before deleting).

## Steps

1. Before adding a tool, check whether an argument on an existing one does the job (`reviews_list` takes `reviewId`, `reviews_write` creates or updates).
2. Set every annotation honestly. A tool that can overwrite data is `Destructive`; one that's safe to repeat is `Idempotent`.
3. Return the smallest useful result. Expensive detail is opt-in (`includeReviews`).
4. Write descriptions that steer: name the tool to call first, and warn when a call can't be undone or shouldn't be retried.
5. When a tool, resource or prompt is added, renamed or changes behaviour, update `ServerInstructions` in the same change.
6. Test the annotations in `ToolListTests` and each error message in the tool's integration tests (`test-design`).

## Example

```csharp
// src/Presentation/Tools/ReviewTools.cs
[McpServerTool(Name = "reviews_delete", Title = "Delete review", ReadOnly = false, Destructive = true, Idempotent = true, OpenWorld = false, UseStructuredContent = true)]
public static async partial Task<DeletedReview> DeleteReview(
```

```csharp
// src/Presentation/Errors/ToolErrorFilter.cs
NotFoundException => $"Not Found: {exception.Message} Check the ids: movies_search, authors_search and reviews_list return valid ones.",
```

## Don't

- Throw from a tool to report a problem the model can fix. Let the filter return an error result.
- Return an unbounded list.
- Name tools after Application classes (`GetMovieByIdQuery`) or use camelCase or dashes.
- Leave `ServerInstructions` describing tools that changed.
- Put internal detail (stack traces, SQL) in an error message.

## Check

``` bash
dotnet test --filter "FullyQualifiedName~ToolListTests|FullyQualifiedName~Errors"
docker compose up -d --build # then try it in the MCP Inspector (mcp-inspector)
```
39 changes: 39 additions & 0 deletions .agents/skills/branches-and-pull-requests/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
name: branches-and-pull-requests
description: Branch naming (feature/, fix/, chore/), how labels and release notes follow from the branch, the pull request template, and how changes get merged in the template repository. Use when creating a branch, writing a commit or opening a pull request.
metadata:
scope: repository
---

# Branches and pull requests

## The rule

Every change goes through a pull request against `main`. A branch ruleset rejects any branch name except `main`, `feature/...`, `fix/...`, `chore/...` and `dependabot/...`. The prefix sets the release notes label automatically (`.github/labeler.yml`), and `.github/release.yml` groups the generated notes by label. The maintainer reviews and squash-merges; nobody merges their own pull request.

| Prefix | For | Label | Release notes section |
| --- | --- | --- | --- |
| `feature/` | Something new for users of the template | `enhancement` | New features |
| `fix/` | Something that was wrong | `bug` | Bug fixes |
| `chore/` | Tidying, dependencies, docs, pipeline | none from the name | Other changes |

Changed files add labels too: `.github/**`, `Dockerfile` or `docker-compose.yml` add `build`; a change made only of Markdown or `docs/` adds `documentation`. Add `breaking-change` by hand when generated solutions or template options change in a way users will notice.

## Steps

1. Branch from an up-to-date `main`: `git switch -c fix/review-validation`. Short, lowercase, hyphenated.
2. Write commit messages as a sentence about the change, in the imperative: "Validate the star rating on update".
3. Open the pull request and fill in `.github/pull_request_template.md`: a two or three sentence summary, the linked issue, the type of change, and whether the shared layers are touched.
4. If shared layers, skills or repository setup changed, say so; the same change has to go to all five templates (`family-sync`).
5. Wait for every required check (`ci-pipeline`). Fix failures with new commits; don't force-push over review comments.

## Don't

- Push to `main`, or use any other branch prefix.
- Merge your own pull request.
- Bundle unrelated changes; they're easier to review apart.
- Add AI attribution lines or tool footers to pull request descriptions.

## Check

The labels appear on the pull request a few seconds after it opens, and every required check is green.
48 changes: 48 additions & 0 deletions .agents/skills/cancellation/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
name: cancellation
description: How cancellation tokens flow from the caller through Mediator, handlers and repositories to EF Core, and how tests prove they are passed on. Use when writing any async method, endpoint, handler or repository call.
metadata:
scope: shared
---

# Cancellation

## The rule

Every async call that can be cancelled is given the token of the request that started it, all the way down to the database. When a caller goes away, the work stops.

| Layer | Where the token comes from |
| --- | --- |
| Presentation | The framework: an endpoint or tool parameter, `HttpContext.RequestAborted`, or (in Lambda) a source built from `context.RemainingTime` |
| Application | `Handle(request, cancellationToken)` |
| Infrastructure | The last parameter of every repository method, passed to every EF Core `...Async` call |

## Steps

1. Add `CancellationToken cancellationToken` as the last parameter of any new async method.
2. Pass it to `sender.Send(request, cancellationToken)`, to every repository call, and to every EF Core method (`ToListAsync`, `CountAsync`, `ExecuteUpdateAsync`, `SaveChangesAsync`).
3. In tests, create a real token and assert it's the one that reached the mock (`unit-tests`):

```csharp
using var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;

// Act
var result = await handler.Handle(command, token);

// Assert
_ = await reviewsRepository.Received(1).UpdateReview(command.Id, command.Stars, token);
```

## Don't

- Use `CancellationToken.None` or `default` outside tests.
- Leave the token out of one call in a chain. Every hop needs it.
- Make the token optional (`= default`) on an interface method.

## Check

``` bash
dotnet build
dotnet test
```
42 changes: 42 additions & 0 deletions .agents/skills/ci-pipeline/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
name: ci-pipeline
description: The Build Pipeline workflow - its jobs, the six required checks, the Summary comment, the monthly schedules, and how to fix a failing check without weakening it. Use when a check fails, when changing .github/workflows, or when explaining what CI does.
metadata:
scope: repository
---

# CI pipeline

## The rule

One workflow, `.github/workflows/build-pipeline.yml`, runs on every pull request, every push to `main`, two monthly schedules and on demand. Jobs run as a chain, and these six are required checks:

| Check | What it does | Fails when |
| --- | --- | --- |
| Build | `dotnet format --verify-no-changes`, `docker build` of the runtime image, `dotnet pack` of the template, the report action's own tests | Style, a warning, a build error, or a template package over 1 MB |
| Test | Runs every test project inside the Dockerfile's `test` stage, reports results and coverage, then installs the packed template and tests a generated solution | A failing test, coverage under 95%, or a template that doesn't generate and pass |
| Static Analysis (actions, csharp) | CodeQL `security-and-quality` on the workflows and the C# | A finding of medium severity or higher |
| Dependency Review | New or updated packages on a pull request | A high or critical vulnerability, or a licence other than MIT, Apache-2.0 or BSD |
| Container Scan | Starts the image and calls it, then Trivy scans the image and the Dockerfile and compose config | The smoke test fails, or a fixable high or critical finding |
| Summary | Posts one comment on the pull request with a row per check, built by `.github/actions/pipeline-report` | Any check above failed |

After those, Release Check and Publish only run on the monthly release (`releases`).

## Steps

1. Read the Summary comment first; it names the unhappy check and links to its details.
2. Reproduce locally where you can: `dotnet format --verify-no-changes`, `dotnet build`, `dotnet test`, the coverage command in `coverage`, `docker build . --target run`.
3. Fix the cause in the code. For security findings, see `security-findings`.
4. Changing the workflow itself: keep each tool as an off-the-shelf action, keep the job names (they're the required check names), and keep everything working in someone else's copy of the template with no secrets or setup.

## Don't

- Skip, disable or mark a test as skipped to get green.
- Lower `COVERAGE_THRESHOLD`, raise `TEMPLATE_MAX_SIZE`, or relax a severity to pass.
- Rename a job without updating the repository's required checks.
- Add a step that needs an account, a paid service or a secret to pass on a fresh copy.
- Push an empty commit to re-run CI. Re-run the job from the Actions page.

## Check

Every required check is green on the pull request, and the Summary comment shows no red rows.
Loading
Loading