8 min readMehdi Hadeli

Per-Service Copilot Instructions for GitHub Copilot CLI: Keep Service Rules Close to the Repo

Part of a three-post series on GitHub Copilot CLI for multi-repo .NET work. If you want the earlier pieces first, start with Multi-Repo Workspace Setup for GitHub Copilot CLI: Context Across .NET Service Boundaries, then read Repo Registry for GitHub Copilot CLI: Service Discovery for Multi-Repo .NET Workflows.

Runnable sample: samples/per-service-copilot-instructions. It includes repo-level instruction files and a validator script that checks each file for the required sections.

Introduction

In a multi-repo .NET workspace, the shared root gives GitHub Copilot CLI the system view. That helps, but it does not cover the local rules that keep one service from drifting into another service's conventions.

Each repo still has its own shape. The API may require a specific validation flow. The Worker may depend on idempotent message handling. The Angular app may consume generated clients. The infrastructure repo may enforce naming rules that should not be guessed from nearby code.

Those rules belong beside the repo that owns them. That is what per-service Copilot instructions are for.

Objectives

The goal is not to write another long README. The goal is to give Copilot CLI a short, repo-local brief that answers the questions a workspace file cannot answer on its own.

A useful service-level instruction file should tell Copilot CLI:

  • where new code belongs in this repo
  • which patterns are required even when older files still show alternatives
  • which files are generated and should not be edited by hand
  • what must be validated before a change is done
  • which shortcuts are not allowed here

If a rule only matters inside one repo and an experienced engineer would still want to know it before making a change, it belongs in the service-level file.

Architecture Overview

The clean split for a .NET-heavy workspace is simple:

  • WORKSPACE.md at the shared root for cross-repo relationships and change order
  • AGENTS.md or .github/copilot-instructions.md inside each repo for service-local rules
  • normal codebase docs for API specs, runbooks, and deeper architecture notes

That split keeps the workspace note focused on the system and the repo note focused on the local editing rules.

For example, a workspace file can tell you which repo owns a DTO and which consumers need to be checked after a contract change. The repo-level file can tell you where endpoint code lives, what mapping pattern is mandatory, and which generated files are off-limits.

~/workspace/platform-workspace/
├── WORKSPACE.md
├── Company.Billing.Contracts/
├── Company.Billing.Api/
├── Company.Jobs.Worker/
├── Company.Admin.Angular/
└── Company.Infrastructure/

This layout keeps shared context at the top and local rules inside the repo that owns them. Without that separation, the workspace note turns into a catch-all and loses value quickly.

Implementation

The most useful service-level instructions are usually the ones that prevent bad local guesses.

Structure That Is Not Obvious

If the repo uses a pattern that is not obvious from the folder tree, write it down directly.

Examples:

  • endpoints are grouped by feature, with validators and mapping in the same folder
  • new code should go through feature services instead of MediatR handlers
  • repositories are internal and should only be used from domain services
  • integration events are published from the application layer, not from controllers

This is the kind of detail that stops Copilot CLI from copying the wrong nearby example.

Critical Invariants

This is the highest-value section.

Use it for rules that must stay true even if the code does not enforce them directly.

Examples:

  • all API failures return the shared problem-details format
  • timestamps crossing service boundaries are UTC
  • queue message contracts remain backward compatible for one deployed version
  • package references to shared contracts stay aligned with the central version file
  • database writes go through the transaction boundary owned by the application service

These are more important than style preferences. They are the rules that prevent behavior from drifting.

Generated-Code Boundaries

Agents will happily edit the wrong file if the source of truth is unclear.

Spell out the boundaries:

  • OpenAPI clients are generated and must not be edited directly
  • TypeScript DTOs are generated from the backend contract package
  • Entity Framework migrations are generated artifacts, but the model configuration is the real change surface
  • Terraform lock files are committed, but module changes should happen in the reusable module, not the environment copy

If a file is generated, say so and say how it gets regenerated.

Local Validation

Do not make the assistant guess what counts as done.

A short validation section is enough:

  • run dotnet test tests/Company.Billing.Api.UnitTests
  • run dotnet test tests/Company.Billing.Api.IntegrationTests for endpoint or mapping changes
  • run npm test -- billing after Angular billing UI edits
  • run terraform validate in the touched environment folder after infra changes

That keeps the definition of done close to the repo.

Forbidden Shortcuts

This is where a lot of practical value lives.

Older files often show patterns that are no longer acceptable. The instruction file should say that plainly.

Examples:

  • do not call shared contracts from the UI through handwritten fetch logic; use the generated client wrapper
  • do not read app settings directly inside controllers; use the typed options class
  • do not publish queue messages from the controller layer
  • do not introduce new AutoMapper profiles in this repo; mapping is explicit by policy

If these rules are not written down, Copilot CLI may reproduce the exact pattern the team is trying to retire.

Example

For Company.Billing.Api, a repo-level file can stay short and specific:

# Company.Billing.Api Copilot Instructions

## Architecture

This API is organized by feature under `src/Features/`.
Each feature folder keeps the endpoint, validator, request model, and mapping together.
Do not add new cross-cutting "Helpers" folders for feature logic.

## Critical Rules

- All outbound DTO mapping goes through `BillingModelFactory`.
- All API errors must use the shared problem-details builder in `src/Infrastructure/Errors/`.
- New timestamps exposed by the API must be UTC and end with `Utc`.
- Controllers stay thin. Business logic belongs in application services.

## Contracts

- Contract types come from `Company.Billing.Contracts`.
- If a contract changes, check compatibility for `Company.Admin.Angular` and `Company.Jobs.Worker`.
- Do not copy contract types into local API models unless there is a versioning reason.

## Validation

- Validators live in the same feature folder as the endpoint.
- Use FluentValidation. Do not add data annotations to request models.

## Tests

- Run `dotnet test tests/Company.Billing.Api.UnitTests`
- Run `dotnet test tests/Company.Billing.Api.IntegrationTests` for endpoint changes

## Avoid

- Do not access configuration with `IConfiguration` inside controllers.
- Do not query the DbContext directly from endpoints.
- Do not add new shared utility folders unless the pattern already exists in `src/Infrastructure/`.

The point is not to describe the whole repo. The point is to record the rules that change how new code should be written.

The same pattern adapts well to other services:

  • a Worker repo can emphasize message compatibility, retry behavior, and idempotency
  • an Angular repo can emphasize generated client boundaries, feature folder layout, and state management rules
  • an infrastructure repo can emphasize naming, environment values, and validation commands

Validation

The sample repo includes a small validator script that checks each repo for AGENTS.md or .github/copilot-instructions.md and fails if the required sections are missing:

#!/usr/bin/env bash

set -euo pipefail

repo_root="${1:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/repos}"
required_sections=("## Architecture" "## Critical Rules" "## Tests")
found_files=0

while IFS= read -r file; do
  found_files=$((found_files + 1))
  echo "Checking $file"
  for section in "${required_sections[@]}"; do
    if ! grep -Fq "$section" "$file"; then
      echo "Missing section '$section' in $file" >&2
      exit 1
    fi
  done
done < <(find "$repo_root" \( -name AGENTS.md -o -path '*/.github/copilot-instructions.md' \) -type f | sort)

if [[ "$found_files" -eq 0 ]]; then
  echo "No instruction files found in $repo_root" >&2
  exit 1
fi

echo "Validated $found_files instruction file(s)"

That keeps the convention lightweight. It does not try to enforce everything. It only checks that each repo has the instructions and the sections people are expected to maintain.

The sample test for that validator is just as small:

bash ./tests/test-instructions.sh

Conclusion

Workspace-level context and repo-level instructions solve different problems.

The workspace file explains how the repos relate. The service-level file explains how to behave once you are inside one repo. That is where the invariants, boundaries, validation steps, and forbidden shortcuts should live.

For multi-repo .NET teams, that is the practical next step after setting up a shared workspace. It keeps the rules close to the code that owns them and makes Copilot CLI more reliable in day-to-day service work.

If you want the earlier pieces in this series, start with Multi-Repo Workspace Setup for GitHub Copilot CLI: Context Across .NET Service Boundaries, then follow it with Repo Registry for GitHub Copilot CLI: Service Discovery for Multi-Repo .NET Workflows. This article fits after those two: first share the workspace, then expose the repos, then teach each repo how it wants changes to be made.