Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-03-03
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-03-03
Udostępnij Udostępnij Kontakt
Wprowadzenie

W poprzednim wpisie nauczyliśmy się pisać skuteczne instrukcje – trwałe zasady które obowiązują przez cały czas. Skills to coś innego: samowystarczalne zdolności na żądanie. Zamiast mówić Copilotowi "zawsze rób X" – uczysz go "gdy ktoś poprosi o Y, wykonaj to w dokładnie taki oto sposób, korzystając z tych konkretnych zasobów".

Skills ogłoszono w GitHub Changelog 18 grudnia 2025 i od razu zrobiły furorę w społeczności. Powód jest prosty – rozwiązują problem który każdy senior developer zna doskonale: jak sprawić żeby agent generował kod zgodny z naszymi wzorcami, a nie z generycznymi przykładami z internetu? Skills dają na to elegancką odpowiedź.

W tym wpisie pokażę jak działają, jak je tworzyć i – to jest mój główny dodatek do dokumentacji – dam Ci gotowy zestaw trzech Skills dla .NET developera do skopiowania i użycia od razu.

Oryginalna dokumentacja, na której bazuje ten wpis: Creating Effective Skills – Awesome GitHub Copilot Learning Hub. Dodatkowe materiały: VS Code Docs – Agent Skills, GitHub Docs – Creating Agent Skills.

Czym skill różni się od instrukcji?

Zanim przejdziemy do tworzenia, warto raz jeszcze mocno zdefiniować różnicę – bo to jest punkt który często gubi ludzi zaczynających przygodę z customizacją Copilota.

🔍 Instrukcja vs Skill – kluczowa różnica
Instrukcja (*.instructions.md) Skill (SKILL.md)
Kiedy aktywna? Zawsze, pasywnie w tle Na żądanie lub gdy agent uzna za odpowiednie
Zawartość Tekst instrukcji Tekst + skrypty + szablony + dokumenty
Uruchamianie Automatyczne (scope-based) Auto (agent) lub ręczne (/nazwa)
Przenośność Tylko Copilot Open standard – Claude Code, inne agenty
Najlepsza do Zasad które zawsze obowiązują Powtarzalnych zadań z bogatym kontekstem

GitHub rekomenduje: instrukcje dla prostych zasad odpowiednich przy każdym zadaniu, skills dla szczegółowych instrukcji które Copilot powinien ładować tylko gdy potrzebne. Nie "albo/albo" – te narzędzia się uzupełniają.

Gdzie skill mieszka?

Skill to folder – nie pojedynczy plik. W środku musi być SKILL.md, opcjonalnie mogą być skrypty, szablony i inne zasoby. Copilot ładuje zasoby z folderu dopiero gdy ich potrzebuje – nie wszystko naraz, dzięki czemu okno kontekstu pozostaje czyste.

🔍 Gdzie tworzyć skills – trzy lokalizacje
Typ Lokalizacja Zakres
Project skill .github/skills/[nazwa-skilla]/ Tylko to repozytorium – współdzielony z teamem
Personal skill ~/.copilot/skills/[nazwa-skilla]/ Wszystkie projekty na Twojej maszynie
Claude Code compat. .claude/skills/[nazwa-skilla]/ Copilot automatycznie tam też zagląda

Ważna ciekawostka: jeśli masz już skille dla Claude Code w katalogu .claude/skills/ – Copilot automatycznie je wykrywa i używa. Open standard działa w praktyce.

Przykładowa struktura projektu ze skills:

.github/
└── skills/
    ├── generate-unit-tests/
    │   ├── SKILL.md
    │   └── templates/
    │       └── test-template.cs
    ├── create-ef-migration/
    │   ├── SKILL.md
    │   └── scripts/
    │       └── verify-migration.sh
    └── scaffold-api-endpoint/
        ├── SKILL.md
        └── templates/
            ├── controller-template.cs
            └── validator-template.cs
Anatomia pliku SKILL.md

Plik SKILL.md to Markdown z YAML frontmatter. Frontmatter zawiera metadane które Copilot czyta zawsze i od razu – nawet jeśli nie załadował jeszcze treści skilla. Na tej podstawie decyduje czy skill jest odpowiedni dla aktualnego zadania.

🔍 Pola frontmatter – co i po co

name
WYMAGANE Unikalny identyfikator skilla. Tylko małe litery i myślniki. Musi być taki sam jak nazwa folderu. Używany jako komenda /nazwa w chatie.
description
WYMAGANE Opis co skill robi i kiedy Copilot powinien go użyć. To jest najważniejsze pole – na jego podstawie agent decyduje o automatycznym załadowaniu. Maks. 1024 znaki. Bądź konkretny – "Use when asked to write unit tests for C# classes" działa lepiej niż "For testing".
inputHint
OPCJONALNE Tekst podpowiedzi wyświetlany w polu chatu gdy skill jest wywoływany jako slash command. Np. [nazwa klasy] [opcje]. Pomaga użytkownikom wiedzieć co wpisać.
showInMenu
OPCJONALNE Domyślnie true. Ustaw false jeśli chcesz żeby skill był dostępny dla agenta automatycznie, ale nie pojawiał się w menu / dla użytkownika.
manualOnly
OPCJONALNE Domyślnie false. Ustaw true jeśli chcesz żeby skill był wywoływany TYLKO ręcznie przez /slash command – agent nie załaduje go automatycznie.
license
OPCJONALNE Licencja skilla. Istotne gdy planujesz go udostępnić publicznie (np. MIT, Apache 2.0).

Kompletny przykład minimalnego pliku:

---
name: generate-unit-tests
description: >
  Generates unit tests for C# classes using xUnit, FluentAssertions,
  and NSubstitute. Use when asked to write tests, create test class,
  add unit tests, or cover code with tests.
inputHint: "[class name or paste class code]"
---

# Treść instrukcji skilla...
⚠️ Opis decyduje o wszystkim – poświęć mu czas

Copilot czyta tylko pole description żeby zdecydować czy załadować skilla. Treść SKILL.md jest ładowana dopiero po tej decyzji. Dlatego opis musi zawierać wszystkie frazy którymi użytkownik może poprosić o wykonanie zadania: "write tests", "create test class", "add unit tests", "cover with tests", "test this class" – im więcej synomimów tym lepiej, w granicach 1024 znaków.

Jak Copilot ładuje skill – architektura kontekstu

Warto rozumieć jak to działa pod spodem, bo to tłumaczy kilka decyzji projektowych które podejmiesz tworząc własne skille.

🔍 Trzyetapowy proces ładowania skilla
  1. Skanowanie metadanych (zawsze) – Copilot czyta frontmatter wszystkich dostępnych skills. Tylko name i description. Lekkie, bez obciążania kontekstu.
  2. Dopasowanie do promptu – gdy Twój prompt pasuje do opisu skilla, Copilot decyduje o jego załadowaniu. Możesz też wymusić ręcznie przez /nazwa-skilla.
  3. Ładowanie treści na żądanie – do kontekstu trafia ciało SKILL.md oraz pliki z katalogu skilla – tylko te do których instrukcja się odnosi. Reszta zostaje na dysku. Dzięki temu możesz mieć wiele bogatych skills bez obaw o przepełnienie okna kontekstu.

Praktyczna konsekwencja: możesz zainstalować dziesiątki skills i Copilot załaduje tylko te które są odpowiednie dla aktualnego zadania. To zupełnie inne podejście niż instrukcje, gdzie wszystko co pasuje do applyTo trafia do kontekstu zawsze.

Jak używać skilla – trzy sposoby

1. Automatycznie przez agenta

Gdy agent mode jest aktywny i Copilot uzna że skill jest odpowiedni – załaduje go sam. Piszesz "napisz testy dla klasy OrderService" i Copilot automatycznie sięga po skill generate-unit-tests.

2. Ręcznie slash commandem

W Copilot Chat wpisujesz /generate-unit-tests i Copilot od razu ładuje skilla. Przydatne gdy chcesz mieć pewność że konkretny skill zostanie użyty, niezależnie od tego jak sformułowałeś pytanie.

// Przykłady ręcznego wywołania:
/generate-unit-tests OrderService

/scaffold-api-endpoint POST /orders/{id}/cancel

/create-ef-migration AddOrderStatusIndex

3. Zarządzanie przez menu Skills

W VS Code Chat wpisz /skills żeby otworzyć menu zarządzania. Możesz tam przeglądać dostępne skills, włączać i wyłączać poszczególne, sprawdzać z którego pliku pochodzą i czytać ich opisy. Przydatne przy debugowaniu – gdy skill się nie odpala, tu sprawdzisz czy w ogóle jest widoczny.

Gotowe Skills dla .NET developera

To jest sekcja którą chciałem dodać od siebie. Zamiast ogólnych przykładów – trzy kompletne, gotowe do skopiowania Skills dla typowego projektu .NET z Clean Architecture. Każdy możesz wziąć i wrzucić do swojego projektu bez żadnych zmian, albo dostosować do swoich wzorców.

📋 Każdy skill to osobny folder. Stwórz katalog .github/skills/[nazwa-skilla]/ i umieść w nim plik SKILL.md oraz opcjonalne zasoby z podfolderów templates/.

Skill #1 – generate-unit-tests

Skill do generowania testów jednostkowych w xUnit z FluentAssertions i NSubstitute, zgodnie z wzorcem AAA.

Struktura:

.github/skills/generate-unit-tests/
├── SKILL.md
└── templates/
    └── test-class-template.cs
📄 SKILL.md
---
name: generate-unit-tests
description: >
  Generates unit tests for C# classes using xUnit, FluentAssertions,
  and NSubstitute. Use when asked to write unit tests, create a test class,
  add tests, cover code with tests, test this method, or write specs for
  a class. Follows AAA pattern (Arrange, Act, Assert).
inputHint: "[ClassName or paste class content]"
---

# Skill: Generowanie testów jednostkowych (.NET)

## Stack testowy
- **Framework:** xUnit 2
- **Asercje:** FluentAssertions 6
- **Mocki:** NSubstitute 5
- **Wzorzec:** AAA (Arrange / Act / Assert)

## Lokalizacja pliku testowego
Umieść plik testowy w projekcie `[NazwaProjektu].Tests`, w podkatalogu
odpowiadającym testowanej klasie. Np. dla `OrderService` z
`Application/Orders/` → plik idzie do `Tests/Application/Orders/`.

## Konwencja nazewnictwa
- Klasa testowa: `[NazwaKlasy]Tests` (np. `OrderServiceTests`)
- Metoda testowa: `[NazwaMetody]_[Scenariusz]_[OczekiwanyWynik]`
  Przykład: `CancelOrder_WhenAlreadyCancelled_ThrowsInvalidOperationException`

## Zasady tworzenia testów

1. **Jeden Assert per test** – jeśli testujesz wiele rzeczy, rozdziel na
   osobne metody.

2. **Sekcje AAA z komentarzami:**
   ```csharp
   // Arrange
   // Act
   // Assert
   ```

3. **Mocki tylko przez NSubstitute:**
   ```csharp
   var repo = Substitute.For();
   repo.GetByIdAsync(orderId).Returns(order);
   ```

4. **Asercje przez FluentAssertions:**
   ```csharp
   result.Should().NotBeNull();
   result.Status.Should().Be(OrderStatus.Cancelled);
   await act.Should().ThrowAsync();
   ```

5. **Nie testuj implementacji – testuj zachowanie:**
   - Złe: `service._repository.Received(1).Save(...)` (weryfikacja internals)
   - Dobre: `result.Status.Should().Be(OrderStatus.Cancelled)`

6. **Happy path + edge cases + błędy:**
   Dla każdej metody wygeneruj przynajmniej:
   - Test dla poprawnego scenariusza (happy path)
   - Test dla niepoprawnych danych wejściowych (np. null, pusta lista)
   - Test dla wyjątku domenowego jeśli metoda go rzuca

## Szablon klasy testowej
Użyj szablonu z pliku [test-class-template.cs](./templates/test-class-template.cs).
📄 templates/test-class-template.cs
using FluentAssertions;
using NSubstitute;
using Xunit;

namespace [Namespace].Tests.[Layer].[Feature];

public class [ClassName]Tests
{
    // Zależności (moki)
    private readonly I[Dependency] _dependency;

    // Testowana klasa
    private readonly [ClassName] _sut;

    public [ClassName]Tests()
    {
        _dependency = Substitute.For<I[Dependency]>();
        _sut = new [ClassName](_dependency);
    }

    [Fact]
    public async Task [MethodName]_[Scenario]_[ExpectedResult]()
    {
        // Arrange
        var input = [setup input];
        _dependency.[Method]([args]).Returns([returnValue]);

        // Act
        var result = await _sut.[MethodName](input);

        // Assert
        result.Should().NotBeNull();
        result.[Property].Should().Be([expectedValue]);
    }

    [Fact]
    public async Task [MethodName]_WhenNullInput_ThrowsArgumentNullException()
    {
        // Arrange
        // (nothing to set up)

        // Act
        var act = async () => await _sut.[MethodName](null!);

        // Assert
        await act.Should().ThrowAsync<ArgumentNullException>()
            .WithParameterName("[paramName]");
    }
}

Skill #2 – scaffold-api-endpoint

Skill do scaffoldowania kompletnego endpointu w minimal API lub kontrolerze – z komendą MediatR, handlerem, walidatorem i testem integracyjnym.

Struktura:

.github/skills/scaffold-api-endpoint/
├── SKILL.md
└── templates/
    ├── command.cs
    ├── handler.cs
    ├── validator.cs
    └── endpoint.cs
📄 SKILL.md
---
name: scaffold-api-endpoint
description: >
  Scaffolds a complete API endpoint for a .NET Clean Architecture project.
  Generates: MediatR command or query, handler, FluentValidation validator,
  and minimal API endpoint registration. Use when asked to add an endpoint,
  create a new API route, add a controller action, implement a feature,
  or scaffold a REST endpoint.
inputHint: "[HTTP method] [route] – e.g. POST /orders/{id}/cancel"
---

# Skill: Scaffolding endpointu API (.NET Clean Architecture)

## Co generujemy
Dla każdego nowego endpointu tworzymy:
1. **Command lub Query** (Application/[Feature]/Commands/ lub /Queries/)
2. **Handler** (w tym samym katalogu co command/query)
3. **Validator** (Application/[Feature]/Validators/)
4. **Endpoint** (API/Endpoints/ lub rejestracja w istniejącym kontrolerze)

## Zasady

### Command vs Query
- **Command** (POST, PUT, PATCH, DELETE) → modyfikuje stan, zwraca `Result` lub `Result`
- **Query** (GET) → tylko czytanie, zwraca `Result`

### Konwencje nazewnictwa
- Command: `[Akcja][Zasób]Command` (np. `CancelOrderCommand`)
- Handler: `[Akcja][Zasób]CommandHandler`
- Query: `Get[Zasób]Query`, `Get[Zasób]ByIdQuery`
- Validator: `[Akcja][Zasób]CommandValidator`

### Obsługa błędów
- Używaj `Result` z Ardalis.Result lub własnego Result type
- Nie rzucaj wyjątków domenowych z handlera – zwracaj `Result.NotFound()`,
  `Result.Forbidden()`, `Result.Invalid()`
- W endpoincie mapuj Result na odpowiedni HTTP status code

### Walidacja
- Każdy command ma odpowiadający validator we FluentValidation
- Validator jest rejestrowany przez `AddValidatorsFromAssembly` – nie ręcznie
- Zasada: waliduj tylko to co możesz zwalidować bez dostępu do bazy

## Szablony
- Komenda: [command.cs](./templates/command.cs)
- Handler: [handler.cs](./templates/handler.cs)
- Validator: [validator.cs](./templates/validator.cs)
- Endpoint: [endpoint.cs](./templates/endpoint.cs)

## Po wygenerowaniu
Przypomnij użytkownikowi o:
1. Rejestracji endpointu w `Program.cs` lub `RouteGroupExtensions`
2. Dodaniu migracji EF jeśli zmiana dotyczy encji
3. Napisaniu testu integracyjnego dla nowego endpointu
📄 templates/command.cs
using Ardalis.Result;
using MediatR;

namespace [ProjectName].Application.[Feature].Commands;

/// <summary>
/// [Opis co robi komenda].
/// </summary>
public record [CommandName](
    [Type] [Property1],
    [Type] [Property2]
) : IRequest<Result<[ReturnType]>>;
📄 templates/handler.cs
using Ardalis.Result;
using MediatR;

namespace [ProjectName].Application.[Feature].Commands;

/// <summary>
/// Handler dla [CommandName].
/// </summary>
public class [CommandName]Handler(
    I[Repository] repository
) : IRequestHandler<[CommandName], Result<[ReturnType]>>
{
    public async Task<Result<[ReturnType]>> Handle(
        [CommandName] request,
        CancellationToken cancellationToken)
    {
        var entity = await repository.GetByIdAsync(
            request.[Id], cancellationToken);

        if (entity is null)
            return Result.NotFound();

        // logika biznesowa

        await repository.SaveChangesAsync(cancellationToken);

        return Result.Success([returnValue]);
    }
}
📄 templates/validator.cs
using FluentValidation;

namespace [ProjectName].Application.[Feature].Commands;

public class [CommandName]Validator : AbstractValidator<[CommandName]>
{
    public [CommandName]Validator()
    {
        RuleFor(x => x.[Property1])
            .NotEmpty()
            .WithMessage("[Property1] jest wymagane.");

        RuleFor(x => x.[Property2])
            .GreaterThan(0)
            .WithMessage("[Property2] musi być większe od zera.");
    }
}
📄 templates/endpoint.cs
using Ardalis.Result.AspNetCore;
using MediatR;

namespace [ProjectName].API.Endpoints.[Feature];

public static class [FeatureName]Endpoints
{
    public static RouteGroupBuilder Map[FeatureName]Endpoints(
        this RouteGroupBuilder group)
    {
        group.MapPost("/{id}/[action]", async (
            [Type] id,
            [CommandName] command,
            ISender sender,
            CancellationToken ct) =>
        {
            var result = await sender.Send(
                command with { Id = id }, ct);
            return result.ToMinimalApiResult();
        })
        .WithName("[EndpointName]")
        .WithSummary("[Krótki opis endpointu]")
        .Produces<[ReturnType]>(StatusCodes.Status200OK)
        .ProducesProblem(StatusCodes.Status404NotFound)
        .RequireAuthorization();

        return group;
    }
}

Skill #3 – create-ef-migration

Skill do tworzenia migracji Entity Framework Core – z weryfikacją nazewnictwa, sprawdzaniem czy nie brakuje indeksów i przypomnieniem o rollback.

📄 SKILL.md
---
name: create-ef-migration
description: >
  Guides creation of Entity Framework Core migrations for .NET projects.
  Use when asked to add a migration, create a database migration, update
  the schema, add an index, modify a table, or run EF Core migration.
inputHint: "[migration name describing the change, e.g. AddOrderStatusIndex]"
---

# Skill: Tworzenie migracji EF Core

## Konwencja nazewnictwa migracji
Nazwa migracji powinna opisywać zmianę (nie być datą ani numerem):
- ✅ `AddOrderStatusIndex`
- ✅ `RenameCustomerEmailColumn`
- ✅ `CreateProductsTable`
- ❌ `Migration001`
- ❌ `Update`
- ❌ `FixBug`

## Komendy

### Dodanie nowej migracji
```bash
dotnet ef migrations add [NazwaMigracji] \
  --project src/[ProjectName].Infrastructure \
  --startup-project src/[ProjectName].API \
  --output-dir Migrations
```

### Zastosowanie migracji
```bash
dotnet ef database update \
  --project src/[ProjectName].Infrastructure \
  --startup-project src/[ProjectName].API
```

### Rollback do poprzedniej migracji
```bash
dotnet ef database update [NazwaPoprzedniej] \
  --project src/[ProjectName].Infrastructure \
  --startup-project src/[ProjectName].API
```

### Usunięcie ostatniej migracji (jeśli niezastosowana)
```bash
dotnet ef migrations remove \
  --project src/[ProjectName].Infrastructure \
  --startup-project src/[ProjectName].API
```

## Checklist przed commitem migracji

Przed commitem sprawdź wygenerowany plik migracji:

1. **Indeksy** – czy dodajesz kolumnę która będzie używana w WHERE lub JOIN?
   Jeśli tak, dodaj indeks:
   ```csharp
   migrationBuilder.CreateIndex(
       name: "IX_Orders_CustomerId",
       table: "Orders",
       column: "CustomerId");
   ```

2. **Nullable vs NOT NULL** – czy nowa kolumna w istniejącej tabeli jest
   nullable lub ma wartość domyślną? Inaczej migracja nie przejdzie na
   niepustej tabeli produkcyjnej.

3. **Down() jest kompletne** – upewnij się że metoda `Down()` poprawnie
   cofa wszystkie zmiany z `Up()`. EF generuje to automatycznie, ale
   warto sprawdzić przy złożonych migracjach.

4. **Nie edytuj istniejących migracji** – jeśli migracja jest już
   zastosowana w jakimkolwiek środowisku, stwórz nową zamiast edytować.

## Ostrzeżenia
- Migracje które zmieniają nazwę kolumny przez `DropColumn` + `AddColumn`
  **tracą dane**. Użyj `RenameColumn` zamiast tego.
- Przy dodawaniu NOT NULL do istniejącej tabeli zawsze podaj
  `defaultValue` lub użyj migracji dwuetapowej.
Jak weryfikować że skill działa?

Po stworzeniu skilla warto sprawdzić czy Copilot go widzi i czy odpala się w odpowiednich sytuacjach. Jest na to kilka sposobów:

1. Sprawdź listę skills

W Copilot Chat wpisz /skills list – zobaczysz wszystkie dostępne skills z ich opisami. Jeśli Twój skill tam jest – Copilot go indeksuje.

2. Sprawdź References po odpowiedzi

Po uzyskaniu odpowiedzi od Copilota rozwiń sekcję References nad odpowiedzią. Jeśli skill był użyty, zobaczysz tam ścieżkę do pliku SKILL.md.

3. Wymuś ręcznie i obserwuj

Wywołaj skill przez /generate-unit-tests OrderService i sprawdź czy wygenerowany kod odpowiada instrukcjom ze skilla. Porównaj z tym co Copilot generuje bez skilla.

⚠️ Skill nie odpala? Najczęstsze przyczyny
  • Zła lokalizacja pliku – sprawdź czy folder skilla jest w .github/skills/[nazwa]/ a nie np. .github/[nazwa]/.
  • Niezgodność nazwy folderu i pola name – nazwa w frontmatter musi być identyczna jak nazwa folderu.
  • Nowe pliki nie są jeszcze zaindeksowane – po dodaniu skilla odczekaj 5-10 minut i przeładuj VS Code.
  • Agent mode wyłączony – automatyczne wykrywanie działa tylko w agent mode. W zwykłym chatie używaj slash commandu.
💪 Zadanie dla Ciebie – wdróż pierwszy skill

Weź skill generate-unit-tests z tego wpisu i zainstaluj go w swoim projekcie:

  1. Stwórz folder .github/skills/generate-unit-tests/
  2. Wklej SKILL.md i dostosuj stack (zmień NSubstitute na Moq jeśli używasz Moq, dodaj własny wzorzec nazewnictwa)
  3. Stwórz folder templates/ i wklej test-class-template.cs – dostosuj namespace do swojego projektu
  4. Scommituj i odczekaj chwilę na indeksowanie
  5. Wpisz w chatie: /generate-unit-tests OrderService i porównaj wynik z tym co Copilot generował wcześniej bez skilla
Podsumowanie

Skills to jeden z najpotężniejszych mechanizmów customizacji Copilota – bo łączą precyzję instrukcji z bogatym kontekstem w postaci szablonów i skryptów, a do tego działają na otwartym standardzie który działa poza ekosystemem samego Copilota.

  • Skill to folderSKILL.md + opcjonalne zasoby. Nie pojedynczy plik.
  • Opis jest kluczowy – na jego podstawie Copilot decyduje o automatycznym załadowaniu. Warto umieścić tam wszystkie synonimy zadania.
  • Trzyetapowe ładowanie – metadane zawsze, treść na żądanie, zasoby tylko gdy potrzebne. Oszczędza okno kontekstu.
  • Dwa sposoby uruchomienia – automatycznie przez agenta lub ręcznie przez /slash-command.
  • Open standard – skills działają też w Claude Code i innych kompatybilnych agentach. Inwestycja się nie deprecjonuje.
  • Trzy gotowe Skills dla .NETgenerate-unit-tests, scaffold-api-endpoint, create-ef-migration – gotowe do skopiowania i użycia.

W kolejnym wpisie pójdziemy o poziom wyżej: budowanie własnych agentów – jak połączyć skills, instrukcje i integracje z narzędziami w jedną spójną konfigurację wyspecjalizowanego asystenta.