From c1dfcb0c596339bc9bc1e394b4cae9615fe3ba2b Mon Sep 17 00:00:00 2001 From: Piotr Zajac Date: Mon, 11 May 2026 11:06:48 +0200 Subject: [PATCH] feat(ai): add AI agent skills for repository workflows Introduces 7 reusable skill prompts following the agentskills.io open standard, stored in .ai/skills/. Each skill encodes a recurring workflow (task orientation, validation, attribute scaffolding, etc.) so any AI agent can follow it consistently across sessions. Thin wrappers in .claude/commands/ expose the skills as Claude Code /slash-commands. AGENTS.md gains an Available Skills section that documents all 7 skills with invocation instructions for every agent type. Closes TASK-20. Co-Authored-By: Claude Sonnet 4.6 --- .ai/skills/add-attribute/SKILL.md | 77 +++++++++++ .../references/extension-model.md | 123 ++++++++++++++++++ .ai/skills/create-branch-pr/SKILL.md | 78 +++++++++++ .../create-branch-pr/assets/pr-template.md | 21 +++ .ai/skills/new-test/SKILL.md | 86 ++++++++++++ .ai/skills/new-test/assets/test-template.md | 94 +++++++++++++ .ai/skills/review-decisions/SKILL.md | 70 ++++++++++ .ai/skills/task-finish/SKILL.md | 84 ++++++++++++ .ai/skills/task-start/SKILL.md | 69 ++++++++++ .ai/skills/validate/SKILL.md | 58 +++++++++ ...I-agent-skills-for-repository-workflows.md | 100 ++++++++++++++ .claude/commands/add-attribute.md | 3 + .claude/commands/create-branch-pr.md | 3 + .claude/commands/new-test.md | 3 + .claude/commands/review-decisions.md | 1 + .claude/commands/task-finish.md | 3 + .claude/commands/task-start.md | 3 + .claude/commands/validate.md | 1 + AGENTS.md | 20 +++ 19 files changed, 897 insertions(+) create mode 100644 .ai/skills/add-attribute/SKILL.md create mode 100644 .ai/skills/add-attribute/references/extension-model.md create mode 100644 .ai/skills/create-branch-pr/SKILL.md create mode 100644 .ai/skills/create-branch-pr/assets/pr-template.md create mode 100644 .ai/skills/new-test/SKILL.md create mode 100644 .ai/skills/new-test/assets/test-template.md create mode 100644 .ai/skills/review-decisions/SKILL.md create mode 100644 .ai/skills/task-finish/SKILL.md create mode 100644 .ai/skills/task-start/SKILL.md create mode 100644 .ai/skills/validate/SKILL.md create mode 100644 .backlog/tasks/task-20 - Create-AI-agent-skills-for-repository-workflows.md create mode 100644 .claude/commands/add-attribute.md create mode 100644 .claude/commands/create-branch-pr.md create mode 100644 .claude/commands/new-test.md create mode 100644 .claude/commands/review-decisions.md create mode 100644 .claude/commands/task-finish.md create mode 100644 .claude/commands/task-start.md create mode 100644 .claude/commands/validate.md diff --git a/.ai/skills/add-attribute/SKILL.md b/.ai/skills/add-attribute/SKILL.md new file mode 100644 index 00000000..b81411ff --- /dev/null +++ b/.ai/skills/add-attribute/SKILL.md @@ -0,0 +1,77 @@ +--- +name: add-attribute +description: Scaffold a new xUnit2 parameter attribute in the Core project. Use this skill when adding a new [AttributeName] that applies to all mock modules (AutoMoq, AutoFakeItEasy, AutoNSubstitute). Encodes the extension model, coding conventions, and test conventions for this repository. Invoke as: add-attribute AttributeName. +--- + +# add-attribute + +Scaffold a new parameter attribute in `Core` following all repository conventions. + +Parameter attributes apply to individual test method parameters and affect how AutoFixture +generates values for them. They derive from `CustomizeAttribute` (AutoFixture.Xunit2) and +override `GetCustomization`. + +See `references/extension-model.md` for an annotated full example. + +## Steps + +**1. Understand the attribute's purpose** + +- What constraint does it place on the generated value? +- Does it accept constructor parameters? What are their types and valid ranges? +- Review existing similar attributes for patterns: + - `[Except]` — excludes specific values + - `[PickFromRange(min, max)]` — constrains to a numeric range + - `[PickFromValues(v1, v2)]` — picks from an explicit list + - `[PickNegative]` — generates negative numbers only + +**2. Create the attribute class** + +Path: `src/Objectivity.AutoFixture.XUnit2.Core/Attributes/Attribute.cs` + +Required conventions: + +- `sealed class` deriving from `CustomizeAttribute` (decision-28) +- `using` directives go **inside** the namespace block (StyleCop SA1210) +- `global::` prefix on all external packages (AutoFixture, etc.) +- `[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]` +- `parameter.NotNull(nameof(parameter))` guard in `GetCustomization` (decision-29) +- No XML doc comments on public members (decision-27); use self-documenting names + +**3. Create the test class** + +Path: `src/Objectivity.AutoFixture.XUnit2.Core.Tests/Attributes/AttributeTests.cs` + +Required structure: + +- `[Collection("Attribute")]` — use the **subject** class name +- `[Trait("Category", "Attributes")]` +- BDD DisplayName on every `[Fact]` and `[Theory]` (decision-24) +- AAA structure with mandatory `// Arrange`, `// Act`, `// Assert` comments (decision-25) +- `[AutoMockData]` placed **above** `[Theory]` +- No `new Fixture()` — inject `IFixture` via test method parameters + +Minimum test coverage: + +- Constructor validation (invalid arguments throw expected exceptions) +- Property values are stored correctly +- `GetCustomization` returns a customization that causes AutoFixture to generate + values satisfying the constraint + +**4. Update AGENTS.md** + +Add a row to the "Parameter Attributes (Core, apply to all modules)" table: + +```text +| `[]` | | +``` + +**5. Run validate** + +Follow `.ai/skills/validate/SKILL.md` to confirm build and tests pass. + +## Rules + +- Add to `Core` only if the attribute applies to all three mock modules +- Never add Moq/FakeItEasy/NSubstitute-specific logic to `Core` +- Build must pass with 0 warnings before marking the task complete diff --git a/.ai/skills/add-attribute/references/extension-model.md b/.ai/skills/add-attribute/references/extension-model.md new file mode 100644 index 00000000..359a3f29 --- /dev/null +++ b/.ai/skills/add-attribute/references/extension-model.md @@ -0,0 +1,123 @@ +# Extension Model Reference — Parameter Attributes + +This document walks through `PickFromRangeAttribute` as a canonical example of how parameter +attributes are implemented in this repository. + +## Annotated source + +```csharp +namespace Objectivity.AutoFixture.XUnit2.Core.Attributes +{ + // ① using directives go INSIDE the namespace block (StyleCop SA1210) + using System; + using System.Reflection; + + // ② global:: prefix on all external packages (decision — C# style) + using global::AutoFixture; + using global::AutoFixture.Kernel; + using global::AutoFixture.Xunit2; + + using Objectivity.AutoFixture.XUnit2.Core.Common; + using Objectivity.AutoFixture.XUnit2.Core.SpecimenBuilders; + + // ③ AttributeUsage restricts to Parameter; AllowMultiple = false is typical + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + // ④ sealed class (decision-28) — all attribute-derived types are sealed + // ⑤ derives from CustomizeAttribute (AutoFixture.Xunit2), not DataAttribute + public sealed class PickFromRangeAttribute : CustomizeAttribute + { + // Public constructor overloads for each supported numeric type + public PickFromRangeAttribute(int minimum, int maximum) + : this((object)minimum, maximum) + { + } + + // ... additional overloads (uint, long, ulong, double, float) follow the same pattern + + // Private canonical constructor validates the range + private PickFromRangeAttribute(object minimum, object maximum) + { + if (((IComparable)minimum).CompareTo((IComparable)maximum) > 0) + { + throw new ArgumentOutOfRangeException( + nameof(minimum), + $"Parameter {nameof(minimum)} must be lower or equal to {nameof(maximum)}."); + } + + this.Minimum = minimum; + this.Maximum = maximum; + } + + public object Minimum { get; } + + public object Maximum { get; } + + // ⑥ override GetCustomization — the single required method from CustomizeAttribute + public override ICustomization GetCustomization(ParameterInfo parameter) + { + // ⑦ parameter.NotNull(nameof(parameter)) — use NotNull() guard (decision-29) + // NotNull() is an extension method from Core/Common/ + // It throws ArgumentNullException when parameter is null + return new FilteringSpecimenBuilder( + new RequestFactoryRelay( + (type) => new RangedNumberRequest(type, this.Minimum, this.Maximum)), + new EqualRequestSpecification(parameter.NotNull(nameof(parameter)))) + .ToCustomization(); + } + } +} +``` + +## Key points + +| # | Rule | Where enforced | +| --- | --- | --- | +| ① | `using` inside namespace | StyleCop SA1210 (build fails if violated) | +| ② | `global::` prefix on external packages | StyleCop SA1135 / team convention | +| ③ | `[AttributeUsage(AttributeTargets.Parameter)]` | Restricts attribute usage to parameters | +| ④ | `sealed` | decision-28 | +| ⑤ | Derives from `CustomizeAttribute` | AutoFixture.Xunit2 integration contract | +| ⑦ | `NotNull()` guard | decision-29 | + +## SpecimenBuilders used in data-narrowing attributes + +| Class | Purpose | +| --- | --- | +| `FilteringSpecimenBuilder` | Wraps a builder and applies it only to matching requests | +| `RequestFactoryRelay` | Creates an AutoFixture request from the parameter type | +| `EqualRequestSpecification` | Matches requests equal to the given `ParameterInfo` | +| `RangedNumberRequest` | Built-in AutoFixture request for a number within a range | + +## Minimal attribute skeleton + +Use this as a starting point for a new attribute: + +```csharp +namespace Objectivity.AutoFixture.XUnit2.Core.Attributes +{ + using System; + using System.Reflection; + + using global::AutoFixture; + using global::AutoFixture.Xunit2; + + using Objectivity.AutoFixture.XUnit2.Core.Common; + + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)] + public sealed class MyNewAttribute : CustomizeAttribute + { + public MyNewAttribute(/* constructor parameters */) + { + // validate and store + } + + public override ICustomization GetCustomization(ParameterInfo parameter) + { + parameter.NotNull(nameof(parameter)); + + // return an ICustomization that applies the constraint + throw new NotImplementedException(); + } + } +} +``` diff --git a/.ai/skills/create-branch-pr/SKILL.md b/.ai/skills/create-branch-pr/SKILL.md new file mode 100644 index 00000000..ce3b908f --- /dev/null +++ b/.ai/skills/create-branch-pr/SKILL.md @@ -0,0 +1,78 @@ +--- +name: create-branch-pr +description: Create a feature branch and open a pull request following repository conventions. Validates branch naming, prefills the PR description from the project template, and walks through the full PR checklist. Use when ready to publish changes. Invoke as: create-branch-pr [TASK-N]. +--- + +# create-branch-pr + +Create a properly named branch and open a PR with the standard description template. + +See `assets/pr-template.md` for the full prefilled PR description. + +## Steps + +**1. Confirm the branch name** + +Format: `/` + +The type must match the Conventional Commits type of the primary commit: + +| Type | Use when | +| --- | --- | +| `feat/` | new attribute or feature | +| `fix/` | bug fix | +| `refactor/` | code restructuring without behaviour change | +| `chore/` | maintenance, tooling, dependency updates | +| `ci/` | CI/CD workflow changes | +| `docs/` | documentation only | + +Examples: `feat/pick-from-list-attribute`, `fix/enumerable-allocation`, `docs/add-skills` + +**2. Create and push the branch** + +```bash +git checkout -b +git push -u origin +``` + +**3. Walk through the PR checklist** + +Confirm each item before opening the PR: + +- [ ] Commit messages follow Conventional Commits (`type(scope): description`) +- [ ] `dotnet build src/Objectivity.AutoFixture.XUnit2.AutoMock.sln` passes with no warnings +- [ ] `dotnet test src/Objectivity.AutoFixture.XUnit2.AutoMock.sln` passes on all framework slices +- [ ] Code coverage is not degraded (Codecov verifies on CI) +- [ ] Mutation score is not degraded (Stryker verifies on CI) +- [ ] New tests follow GIVEN/WHEN/THEN naming and AAA structure +- [ ] No new `[SuppressMessage]` without a justification comment +- [ ] No `// TODO:` comments added — open a GitHub issue instead +- [ ] No new dependencies incompatible with the MIT license + +**4. Open the PR** + +Use the prefilled template from `assets/pr-template.md`: + +```bash +gh pr create \ + --title "(scope): " \ + --body "$(cat .ai/skills/create-branch-pr/assets/pr-template.md)" +``` + +In the PR body: + +- Replace `Closes #` with the actual GitHub issue number if applicable +- Add a label to the PR after creation (`gh pr edit --add-label "