GitHub Copilot multi-repo instructions

GitHub Copilot Multi-Repo Instructions: Sharing Skills, Agents, and Conventions Across Repos

TL;DR: GitHub Copilot writes idiomatic code, but doesn’t know your team’s conventions, pitfalls, or cross-repo workflows. At Arinco, we addressed this using GitHub Copilot multi-repo instructions — a logical structure of markdown files (per-repo instructions, a shared Copilot repo, skills, and agents) committed to source control, so both Copilot and developers know exactly where conventions, procedures, and guardrails live. The result: Copilot Agents match what a senior team member would plan, write and test, along with quality-of-life skills like raising a PR is now a single command (diff analysis, readiness checks, work item links, creation in Azure DevOps, all automated).

Table of Contents

The Problem: Copilot Doesn’t Know Your Conventions

Out of the box, Copilot is trained on public code, for someone elses project. It’ll generate perfectly adequate C#, but it doesn’t know that your team uses primary constructors, that your handlers follow a two-phase validate-then-write flow, or that injecting a shared DbContext into parallel code will blow up at runtime.

That problem compounds across multiple repositories. When your platform spans 15+ repos (shared contracts, pipeline templates, database schemas that need to stay in sync) conventions drift fast. One repo uses file-scoped namespaces, another doesn’t. Pipeline YAML gets copy-pasted with subtle mutations. The wiki that’s supposed to keep things aligned hasn’t been updated in six months.

“How do you give Copilot enough context about your specific platform that its suggestions actually match what a senior team member would write?”

The answer turned out to be surprisingly simple: tell it. In writing. In the repo.

The Solution

What we wanted was a way to define conventions once, share them across every repo, and have them actively enforced through the tooling developers already use. The answer at Arinco was a set of GitHub Copilot multi-repo instructions – shared markdown files, committed to source control, applied consistently across every repository through a multi-root VS Code workspace.

Solution Level 1: Per-Repo Copilot Instructions

VS Code’s Copilot extension looks for a .github/copilot-instructions.md file in each repository. Whatever you put in there becomes part of the context Copilot uses when generating suggestions, answering questions, or writing code in that repo.

We started small. Just the basics:

				
					## Build and Test

dotnet build
dotnet test
dotnet test --filter "FullyQualifiedName~MyTestName"

## C# Conventions

- PascalCase for public members, _camelCase for private fields
- Async methods end with `Async` and accept `CancellationToken`
- Use primary constructors for services and repositories
- Repository pattern is deferred-save: stage changes, commit once
Even this much made an immediate difference. Copilot stopped suggesting Task.Run for async operations. It started using CancellationToken in method signatures without being asked. It used _camelCase for private fields instead of whatever the training data preferred that day.
But real value came when we started adding things that aren’t obvious. The pitfalls:
## Pitfalls

- Never inject a shared DbContext into parallel fan-out code.
  Use IDbContextFactory and create a context per-operation.
  A shared context throws "A second operation was started on this
  context instance" as soon as two tasks overlap.

- NuGet package versions for message contracts must match across
  producer and consumer repos. Version skew causes silent
  deserialization failures that surface as dead-letter messages
  days later.

## Anti-Patterns (NEVER DO THESE)

1. Do not use lock tokens for message deletion, use MessageId
2. Do not call OperationResult.Failure() without passing the exception
3. Do not put @code blocks in .razor files, use .razor.cs code-behind

				
			

That pitfalls section has probably prevented more bugs than any linter we could have configured. When someone asks Copilot “help me process these schools in parallel,” it now knows to suggest IDbContextFactory instead of injecting the context directly. Because we told it what happens when you don’t.

Per-repo instructions solve the single-repo problem. The next question is how to make Copilot see every repo on the platform at once.

Solution Level 2: The Multi-Root Workspace

This is the bit that ties everything together, and it’s easy to overlook because it’s just a JSON file. But without it, nothing else in this post works.

VS Code supports multi-root workspaces. A single window that opens multiple repository folders side by side. We maintain a .code-workspace file that includes every repo on the platform:

				
					{
  "folders": [
    { "name": "Copilot Shared", "path": "../Copilot.Shared" },
    { "path": "../Integration.Core" },
    { "path": "../Integration.Templates" },
    { "path": "../Integration.Engine" },
    { "path": "../Integration.Acquisition" },
    { "path": "../Integration.MessageContracts" },
    { "path": "../Integration.Dashboard" },
    { "path": "../Integration.LearningSystems" }
  ]
}

				
			

This gives you three things for free:

  1. Cross-repo search: Ctrl+Shift+F finds all usages of a message type across producers and consumers without switching windows
  2. Copilot has full visibility: it can see the contracts repo, the consumer, the producer, and the database schema all at once when answering questions
  3. The shared Copilot repo is automatically discovered: because it’s a folder in the workspace, VS Code picks up its.github/copilot-instructions.md and applies it as workspace-level context

That third point is the missing ingredient. The shared Copilot repo only works because it’s open in the workspace. If you clone it but don’t include it in your workspace file, Copilot never sees it. The workspace file is what makes the inheritance hierarchy real.

We keep the workspace file in the shared repo itself and treat it as a first-class artefact. When a new integration repo is added to the platform, the workspace file gets updated in the same PR. New team members clone everything, open the workspace file, and they’re immediately working with full platform context. Both for themselves and for Copilot.

The workspace gives Copilot access to every repo. What it still doesn’t have is a place for conventions that belong to no single repo, but to all of them.

Solution Level 3: A Shared Repo for Multi-Repo Copilot Instructions

Why a Dedicated Repo

Per-repo instructions handle component-specific conventions well, but we had a problem: conventions that span the entire platform. Git workflow. Security requirements. Environment promotion order. The architectural data flows across repos. These don’t belong in any single repo. They belong everywhere.

The approach we took was to create a dedicated repository that contains nothing but Copilot customisation files. No application code. No infrastructure. Just context for the AI.

Repository Structure

				
					Copilot.Shared/
├── .github/
│   ├── copilot-instructions.md
│   ├── agents/
│   │   ├── dead-letter-investigator.agent.md
│   │   ├── pr-evaluator.agent.md
│   │   └── architecture-review.agent.md
│   ├── skills/
│   │   ├── field-mapping-change/
│   │   │   └── SKILL.md
│   │   ├── service-bus-contract-change/
│   │   │   └── SKILL.md
│   │   └── pipeline-template-upgrade/
│   │       └── SKILL.md
│   ├── instructions/
│   │   ├── branch-targeting.instructions.md
│   │   └── multi-repo-freshness.instructions.md
│   ├── prompts/
│   │   ├── release-checklist.prompt.md
│   │   └── hotfix-checklist.prompt.md
│   └── hooks/
│       ├── team-guardrails.json
│       └── scripts/
└── README.md

				
			

This repo is added as a folder in a VS Code multi-root workspace alongside all the integration repos. Because Copilot discovers.github/copilot-instructions.md automatically, the shared instructions apply across the entire workspace without any configuration.

The Shared Instructions File as a Routing Table

The shared copilot-instructions.md acts as a routing table. It describes the overall architecture, lists every available skill and agent, defines cross-project conventions, and links out to each repo’s own instructions for component-specific rules:

				
					## Architecture

Source API → Azure Functions (Acquisition)
  → Service Bus (contracts in MessageContracts repo)
  → Azure Functions (Engine)
  → SQL Database (Common Data Model)
  ← Read API (per-domain Function App + APIM policy)
  ← Consumers via APIM

## Conventions

- Use Managed Identity and Entra ID auth, no secrets in source
- Secrets in Key Vault, non-secret config in App Configuration
- Async I/O methods end with `Async` and take `CancellationToken`
- Service Bus processing uses explicit dead-letter for data errors
  and retry for transient failures
- Structured logging with named parameters and correlation scope

## Available Skills

| Task                                    | Skill                      |
|-----------------------------------------|----------------------------|
| Add a field end-to-end (API → DB)       | field-mapping-change       |
| Modify a Service Bus message contract   | service-bus-contract-change|
| Bump pipeline template version          | pipeline-template-upgrade  |
| Implement a new handler end-to-end      | engine-new-handler         |

				
			

Inheritance: Workspace Defaults Plus Per-Repo Overrides

This creates an inheritance hierarchy: workspace-level defaults from the shared repo, overridden or extended by per-repo instructions. Same concept as .editorconfig inheritance but for AI-assisted development.

Shared instructions are great for what the platform looks like, but they don’t capture how to make changes that ripple through it. That’s where skills come in.

Solution Level 4: Skills (Procedural Knowledge)

Static instructions tell Copilot how to write code. Skills tell it what to do for specific multi-step tasks. The kind of cross-cutting changes that normally require tribal knowledge.

What a Skill Looks Like

A skill is a SKILL.md file with YAML frontmatter that describes when to use it, and a step-by-step procedure covering which repos to touch, in what order, and what can go wrong:

				
					---
name: field-mapping-change
description: "Add or modify a field flowing from the source API through
  Service Bus to the Common Data Model database. Crosses 3 repos in strict order."
argument-hint: "Describe the field name, source, target column, and
  which entity types it applies to"
---
The frontmatter is the part Copilot reads to decide whether to load the skill. The procedure body is what it follows once it does.
Example: A Field Mapping Change Across Three Repos
# Field Mapping Change

## When To Use
- Adding a new field from the source API to the database
- Renaming or correcting an existing mapped attribute
- Any change that touches the DTO → message → mapper → SQL chain

## Full Change Chain

A complete field mapping change crosses three repos in a specific
order. Always publish the contract change first, then update the
consumer (Engine) before the producer (Acquisition).

## Procedure

### Step 1 - Update the message contract (MessageContracts repo)
1. Add the property to the relevant DTO
2. Bump the NuGet package version
3. Publish the package

### Step 2 - Update the database schema (Data model repo)
1. Add the column to the relevant table
2. Rebuild the DACPAC
3. Check if managed identity permissions need updating

### Step 3 - Update the processing engine (Engine repo)
1. Bump the MessageContracts NuGet version
2. Add the field to the mapper
3. Update unit tests

### Step 4 - Update the acquisition layer (Acquisition repo)
1. Bump the MessageContracts NuGet version
2. Populate the new field from the API response
3. Update unit tests

### Step 5 - Cross-repo validation
1. Verify NuGet versions match across all consuming repos
2. Run tests in each repo
3. Confirm the field flows end-to-end

## Common Mistakes

- Adding the SQL column but not the mapper → field is always null
- Bumping the contract in Engine but not Acquisition → version skew
- Forgetting managed identity SQL permissions on the new column

				
			

Why the Ordering Matters

The value here isn’t the individual steps. Any senior developer could figure those out. The value is the ordering constraints. “MessageContracts first, then Engine, then Acquisition” isn’t intuitive to someone new, but getting it wrong means messages silently fail deserialization and pile up in the dead-letter queue for days before anyone notices.

We have thirteen skills covering everything from schema changes to pipeline template upgrades. When someone asks Copilot to help with a task that matches a skill, it loads the procedure and follows it rather than improvising.

Skills cover predictable, procedural work. Agents take this further into investigative and operational territory: judgement calls, tool use, and live system access.

Solution Level 5: Agents (Specialist Personas)

Skills handle procedural work. Agents handle investigative and operational tasks that require judgement, tool access, and domain expertise.

What an Agent Looks Like

An agent is defined in a .agent.md file with YAML frontmatter specifying its name, description, tools, and optionally a preferred model:

				
					---
name: "Dead Letter Investigator"
description: "Triage Service Bus dead letter queue messages.
  Classify failures and recommend resolution paths."
tools: [read, search, execute]
model: "GPT-4o (copilot)"
---
You are a Service Bus dead letter queue specialist. Your job is to
triage dead-lettered messages and determine the correct resolution
path without writing any code.

				
			

We built seven agents. Two of them turned out to be genuine quality-of-life wins that the team actually talks about.

Dead Letter Queue Triage

Before the agent, triaging a dead-letter message was a 20-30 minute exercise: peek the message in a terminal, read the JSON body, figure out which handler threw, check deployment history, maybe grep App Insights, and eventually decide whether to resubmit. Nobody enjoyed it.

The agent classifies every message into one of three categories:

Class

Type

Resolution

A – Data error

Bad source data, no code fix needed

Don’t resubmit. Escalate to data team.

B – Transient infra error

Timeout, connection failure, lock expiry

Resubmit and monitor.

C – Code/config bug

Fix required before resubmit

Deploy the fix first. Resubmit only the latest message per entity, purge duplicates.

The categories matter more than they look. Each one maps to a different action, and the wrong action (resubmitting a poison message, or escalating a transient blip) creates more work, not less.

The agent can peek messages live from the DLQ without consuming them, check NuGet package versions across repos (version skew is the number one cause of deserialization dead-letters), cross-reference recent deployments, and produce a verdict.

The real value is the production-hardened gotchas baked into its instructions. Things we learned from actual incidents. Like: when a code bug causes duplicate messages to accumulate over days (same entity, resubmitted hourly), you keep only the latest per entity and purge the rest rather than resubmitting hundreds of duplicates and overwhelming the handler.

What used to be 20 minutes of context-switching is now a 30-second conversation.

PR Creation End-to-End

The second agent automates the PR workflow in three phases:

Phase 1: reads the git diff, identifies what changed, extracts the work item ID from the branch name, and generates a structured PR description.

Phase 2: validates readiness. Correct branch target, NuGet versions aligned, builds pass, no secrets in the diff, pipeline template tag is current. Pass/fail verdict with specific blockers.

Phase 3: if the user requests it, actually creates the PR in Azure DevOps using MCP (Model Context Protocol) server tools. Resolves the repository GUID, constructs the PR with the generated description, links work items, and returns the URL.

One implementation detail worth calling out: the Azure DevOps MCP tool silently fails if you pass a repository name instead of a GUID. So, the agent always resolves the GUID first by listing repos and filtering. That kind of gotcha is exactly what belongs encoded in an agent rather than in someone’s memory.

What used to be: write a description → manually check versions → open the browser → create the PR → link the work item, is now: “raise a PR for this branch” → done.

The Inheritance Hierarchy

Putting it all together, the context hierarchy looks like this:

				
					Copilot.Shared/.github/copilot-instructions.md     ← platform-wide defaults
  ├── instructions/*.instructions.md                ← contextual guardrails
  ├── skills/*/SKILL.md                             ← procedural checklists (loaded on demand)
  ├── agents/*.agent.md                             ← specialist personas
  ├── prompts/*.prompt.md                           ← reusable one-off templates
  └── hooks/                                        ← automated interception

Repo-A/.github/copilot-instructions.md              ← repo-specific overrides
Repo-B/.github/copilot-instructions.md              ← repo-specific overrides

				
			

Workspace-level defaults apply everywhere. Repo-level instructions override or extend for their specific context. Skills and agents are loaded on demand when the task matches. Hooks intercept in real-time.

How We Maintain It

The most common question: “doesn’t this go stale?”

We treat customisation files like code. If you change a convention (say, you switch from Allman braces to file-scoped namespaces) you update the instruction in the same PR. If an agent’s classification rules miss a new failure mode, you add it when you discover it.

Because it’s all in a git repo, it goes through the normal PR workflow. Changes are reviewable, attributable, and reversible. When someone improves a skill or adds a production gotcha to an agent, everyone gets it on their next git pull.

The shared repo also acts as onboarding documentation by accident. New team members read the skills to understand cross-repo change procedures. They read the agents to understand operational workflows. They read the instructions to understand conventions. None of it was written as documentation – it was written for Copilot – but it turns out that context useful for an AI is also context useful for a human.

The Honest Tradeoffs

  • It’s not magic. Copilot still gets things wrong. But it gets things wrong in the direction of your conventions rather than the direction of random Stack Overflow answers.
  • Maintenance is real. Someone has to update the files when patterns change. We’ve found it’s less effort than maintaining a wiki because the feedback loop is tighter: wrong instruction → wrong suggestion → fix the instruction.
  • Model limits apply. Very large instruction files can exceed context limits. Keep them focused. The shared file is a routing table, not an encyclopedia.
  • Skills don’t replace judgement. They encode the mechanical steps. The “should we do this at all?” question still needs a human.

Closing Thought

The thing that surprised us most was how small the investment was relative to the payoff. The shared repo is about 20 markdown files. Each repo’s instruction file is a page or two. The agents are a couple of pages each. Total effort to set up: a few days. Total effort to maintain: a few minutes per PR when conventions change.

The quality-of-life agents (DLQ triage and PR creation) turned out to be the things the team actually talks about. Not because they’re technically impressive, but because they removed the tasks that were just annoying enough to dread but not important enough to properly automate with traditional tooling. Copilot agents sit in a sweet spot: cheaper to build than an internal tool, easier to maintain than a runbook, and they get better as you encode more production lessons into them.

If you’re using Copilot but haven’t written a copilot-instructions.md yet, start there. Tell it your conventions. Tell it your pitfalls. Tell it the things that would take a new team member a month to learn. You’ll be surprised how much better the suggestions get when the AI actually knows how your team writes code. If you want advice or support, contact us.

More insights

Get started on the right path to cloud success today. Our Crew are standing by to answer your questions and get you up and running.