Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions .claude/commands/implement-unblocked.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
---
description: Assign every unblocked, unassigned issue not awaiting a decision or a major version to the gh user, move it to In Progress, and implement each one as a PR from its own worktree via sub-agents.
argument-hint: "[issue numbers to restrict to] [--dry-run]"
allowed-tools: Bash(gh:*), Bash(git:*), Agent
---

# Implement unblocked issues

You are the **fanning agent** described under *Worktrees* in `AGENTS.md`. You provision one worktree
per issue up front, then hand each sub-agent a path that already exists. Sub-agents only do the
work. They never provision.

Arguments: `$ARGUMENTS`

* Bare numbers restrict the run to those issues. They must still pass the filter in step 2.
* `--dry-run` stops after step 2. Print the issues that would be picked up and change nothing.

The repository is `PaulTrampert/PTrampert.SimplePatch`.

## 1. Preflight

If any check fails, stop and tell the user how to fix it. Don't work around a failure.

* `gh auth status` must list the `project` scope, which moving an issue on a project board needs.
If it's missing, ask the user to run `! gh auth refresh -s project`.
* Bring `main` up to date so every worktree starts from the latest `main`, not from whatever this
clone last fetched. Find the primary checkout from the shared git directory so this works from
any worktree. Then fetch and pin the commit the whole batch branches from:

```bash
PRIMARY="$(dirname "$(git rev-parse --path-format=absolute --git-common-dir)")"
git -C "$PRIMARY" fetch origin main
BASE="$(git -C "$PRIMARY" rev-parse origin/main)"
git -C "$PRIMARY" fetch . origin/main:main # fast-forward local main; refuses if it has diverged
```

If the fetch fails, stop. If git refuses the fast-forward because local `main` has commits that
aren't on `origin/main`, stop and report those commits. Never reset `main`. If git refuses only
because `main` is checked out in a worktree, and that tree is clean, run
`git merge --ff-only origin/main` there. If it isn't clean, leave it and note it for the final
report. Either way, the worktrees branch from `$BASE`.

## 2. Find the issues

An issue qualifies when all of the following hold:

* It is **open**.
* It has **no assignees**.
* It has **no open blocking issue** (GitHub issue dependencies). A closed blocker no longer blocks.
* It has **no open pull request** that will close it. Someone is already working on it.
* It isn't labelled **`needs decision`**, meaning it isn't defined well enough to implement yet.
* It isn't labelled **`Breaking Change`**, meaning it has to wait for a major version.

Naming an issue in the arguments doesn't override the label rules. The label has to be removed
first.

```bash
gh api graphql --paginate -f query='
query($endCursor: String) {
repository(owner: "PaulTrampert", name: "PTrampert.SimplePatch") {
issues(states: OPEN, first: 100, after: $endCursor) {
pageInfo { hasNextPage endCursor }
nodes {
number
title
labels(first: 20) { nodes { name } }
assignees(first: 1) { totalCount }
blockedBy(first: 50) { nodes { number state } }
closedByPullRequestsReferences(first: 10, includeClosedPrs: false) { nodes { number state } }
}
}
}
}' --jq '.data.repository.issues.nodes[]
| select(.assignees.totalCount == 0)
| select([.labels.nodes[].name] | (index("needs decision") or index("Breaking Change")) | not)
| select([.blockedBy.nodes[] | select(.state == "OPEN")] | length == 0)
| select([.closedByPullRequestsReferences.nodes[] | select(.state == "OPEN")] | length == 0)
| {number, title, labels: [.labels.nodes[].name]}'
```

Print the list with each issue's number and title. If the list is empty, say so and stop. If
`--dry-run` was given, stop here.

## 3. Claim them

For each issue:

1. Assign it to the account `gh` is logged in as:
`gh issue edit <n> --repo PaulTrampert/PTrampert.SimplePatch --add-assignee @me`.
2. Move it to **In Progress** on every project board it belongs to. Look up the issue's project
items and each project's `Status` field:

```bash
gh api graphql -f query='
query($n: Int!) {
repository(owner: "PaulTrampert", name: "PTrampert.SimplePatch") {
issue(number: $n) {
projectItems(first: 10) {
nodes {
id
project {
id
title
field(name: "Status") {
... on ProjectV2SingleSelectField { id options { id name } }
}
}
}
}
}
}
}' -F n=<n>
```

Then set the option whose name is `In Progress`, compared case-insensitively:

```bash
gh project item-edit --id <item id> --project-id <project id> \
--field-id <field id> --single-select-option-id <option id>
```

If the issue isn't on any project, or a project has no `In Progress` option, note it for the
final report and carry on. Don't add the issue to a project or invent a status.

Claim every issue before starting any implementation, so the board shows the whole batch at once.

## 4. Provision the worktrees

Worktrees go inside the primary checkout, under `.claude/worktrees/`, which git ignores. Never
create one beside the checkout. `$PRIMARY` and `$BASE` come from step 1:

```bash
WORKTREES="$PRIMARY/.claude/worktrees"
```

For each issue, pick a branch name. Use `bugfix/<n>-<slug>` if the issue has the `bug` label, and
`feature/<n>-<slug>` otherwise. `<slug>` is the title in lower case, reduced to `[a-z0-9-]`, and
cut to a few words. Then run:

```bash
git -C "$PRIMARY" worktree add "$WORKTREES/issue-<n>" -b <branch> "$BASE"
```

If the branch or the directory already exists, don't reuse or overwrite it. Skip that issue and
report it. Never switch the branch of an existing worktree.

## 5. Implement, one sub-agent per issue

Launch every sub-agent **in a single message** so they run concurrently. Use the `general-purpose`
agent type. Don't pass `isolation`, because the worktree already exists. Give each one this
prompt, filled in:

> You are implementing GitHub issue #<n> ("<title>") in `PaulTrampert/PTrampert.SimplePatch`.
>
> Work only in the worktree at `<absolute worktree path>`. It is already checked out on branch
> `<branch>`, from `main` at `<base commit>`. Use absolute paths or `git -C` for everything. Don't
> create another worktree and don't switch branches. If `git branch --show-current` there isn't
> `<branch>`, stop and report that.
>
> 1. Read `AGENTS.md` in the worktree and follow it. It holds the project's coding standards, test
> policy, and PR conventions.
> 2. Read the issue in full: `gh issue view <n> --repo PaulTrampert/PTrampert.SimplePatch --comments`.
> If it points to a design document under `docs/`, read that too. A design document on `main`
> is settled, so implement it as written. If the issue can't be delivered as written, or needs a
> decision that neither the issue nor the design doc makes, stop and report why. Don't guess,
> and don't deviate from the design.
> 3. Implement the issue, with a regression test that fails before the fix. Keep the change scoped
> to this issue. Any change to the public API must be additive. If the issue can't be done
> without a breaking change, stop and report that.
> 4. `dotnet build` must pass with no new warnings, and `dotnet test` must pass.
> 5. Commit in meaningful steps. End each commit message with
> `Co-Authored-By: Claude <noreply@anthropic.com>`.
> 6. Push with `git -C <path> push -u origin <branch>`, then open a PR against `main` with
> `gh pr create`. Start the title with `(PATCH): `, `(MINOR): ` or `(MAJOR): `, as `AGENTS.md`
> describes. The body explains what the diff doesn't make obvious, gives the test results,
> includes `Closes #<n>`, and ends with
> `馃 Generated with [Claude Code](https://claude.com/claude-code)`.
>
> Finish with a short report: the PR URL (or why there isn't one), what you tested and how, and
> anything the reviewer should look at first.

If your own system prompt specifies a co-author attribution, use it in place of `Claude` in the
co-author line.

## 6. Report

When every sub-agent has finished, give the user one table showing each issue, its branch, and its
PR link or the reason there isn't one. Also list any issue that couldn't be moved to *In Progress*.
Leave the worktrees in place, because they hold the branches under review. Give the command to
remove one once its PR merges: `git worktree remove <path>`.

If a sub-agent fails, don't unassign its issue or move it back on the board. Report the failure and
leave the decision to the user.
112 changes: 112 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# AGENTS.md

Guidance for coding agents working in this repository. `CLAUDE.md` is a symlink to this file, so
edit this one.

## What this is

PTrampert.SimplePatch is a .NET library for flat PATCH request bodies. It tells a property that was
omitted apart from one that was explicitly set to `null` or to a value. A controller takes
`[FromBody] IPatchObject<TWriteModel>`, and the library builds the concrete class at runtime.

## Layout

| Project | Target | Purpose |
| --- | --- | --- |
| `PTrampert.SimplePatch` | net8.0 | Core package: `Optional<T>`, `IPatchObject<T>`, `PatchClassBuilder`, JSON converters, validation |
| `PTrampert.SimplePatch.Schema` | net8.0 | `PatchSchemaTransform`: the OpenAPI schema rewrite shared by both integration packages |
| `PTrampert.SimplePatch.Swashbuckle` | net8.0 | Swashbuckle schema filter |
| `PTrampert.SimplePatch.OpenApi` | net10.0 | `Microsoft.AspNetCore.OpenApi` schema transformer. It is net10.0 only because it needs `GetOrCreateSchemaAsync`. |
| `*.Test` | match their subject | NUnit test projects, one per shipped package (except `Schema`, which the integration tests cover) |
| `PTrampert.SimplePatch.Sample` | net8.0 | Sample web API, not packed |

Docs are built with docfx (`docfx.json`, `index.md`, `docs/`). The API reference is generated
into `api/` from XML doc comments. `README.md` is packed into every NuGet package, so it is the
public face of the library on nuget.org.

## How it works

- `PatchClassBuilder.GetPatchClassFor(type)` generates C# source with CodeDom, compiles it with
Roslyn into its own in-memory assembly, and caches the result in a **static** dictionary.
`PatchClassBuilder.Instance` is the only instance to use. The public constructor is obsolete.
- The generated class has one `Optional<T>` property per patchable source property and a `Patch`
method. Records are patched with a `with` expression. Other types go through constructor binding
and an object initializer.
- Because the generated assembly is separate, it can only reference **public** types and public
setters or init accessors. Non-public source types throw `NotSupportedException`.
- Source property attributes are carried over: `[JsonConverter]` becomes
`[OptionalConverter]`, `[JsonPropertyName]` is copied, and each `ValidationAttribute` becomes an
`[OptionalValidation(type, index)]` that runs only when the property is present.
- `JsonOptionsExtensions.AddSimplePatchConverters` registers `OptionalJsonConverterFactory` and
`PatchJsonConverterFactory`.
- The OpenAPI packages build the patch schema from the **source model's** schema, not from the
generated class, so the two contracts can't drift. See
`docs/proposals/openapi-contract-generation.md` for the design record.

## Build and test

The SDK is pinned by `global.json` (10.0.x, rolling forward). Run from the repository root:

```bash
dotnet build
dotnet test
```

To build the docs, run `dotnet tool restore`, then `dotnet docfx docfx.json`. Output goes to
`_site/`, which git ignores.

CI (`.github/workflows/dotnet-library.yml`) uses a shared workflow from
`PaulTrampert/github-workflows` to build, test, and publish to NuGet on merge to `main`.

## Conventions

- **PR titles must start with `(MAJOR)`, `(MINOR)` or `(PATCH)`.** A CI check enforces this, and
the prefix drives the released version. Use the form `(PATCH): Short imperative summary`.
Choose the level by the change's effect on the public API of the shipped packages.
- PR descriptions explain cause, fix, and the alternatives that were rejected, and finish with test
results. Reference the issue with `Closes #N`.
- Public API changes should be additive within a major version. Mark a member `[Obsolete]` with a
link to the tracking issue before removing it.
- Every public member has XML doc comments. `GenerateDocumentationFile` is on, and the docs site is
built from them.
- Comments explain *why*, especially constraints that aren't obvious from the code (for example,
why the generated assembly can only see public types). Match the comment density of the
surrounding code.
- Tests use NUnit 4 with the constraint model (`Assert.That`). Test-only types go in the test
project's `TestObjects/` folder. Add a regression test that fails before the fix.
- Don't let the build's warning set grow.
- Keep `README.md` and `docs/` in step with behaviour changes.
- Name branches `bugfix/<issue>-<slug>` for bugs and `feature/<issue>-<slug>` otherwise. Never commit
directly to `main`.
- Deliver one issue per PR, small enough for a reviewer to hold in their head.
- An agent never merges a PR unless the user directly asks it to.

## Worktrees

- **Worktrees go under `.claude/worktrees/` inside the primary checkout**, which git ignores.
Never create one beside the checkout. Use
`git worktree add .claude/worktrees/<name> -b <branch> origin/main`. `EnterWorktree` and
sub-agent isolation already put them there. Remove a worktree with `git worktree remove` once
its branch is merged.
- **A top-level agent** creates its own worktree before making any edits and works there, not in
the primary checkout.
- **A sub-agent** works in the worktree it was handed and doesn't provision another. A sub-agent of
a worktree-isolated session usually can't use a new worktree anyway.
- **Never switch the branch of a worktree you didn't create.** If a worktree is on the wrong
branch for your task, say so and stop. Check `git branch --show-current`, not the directory name.
- **When fanning work out across several issues**, the fanning agent creates one worktree per
issue up front, each already on the right branch. It then hands each sub-agent a path that
already exists.

## Repository metadata

- `CLAUDE.md` is a symlink to this file. Edit `AGENTS.md`, and never replace the symlink with a
copy.
- `.claude/commands/implement-unblocked.md` is the `/implement-unblocked` slash command. It takes
every open, unassigned issue that meets all of these conditions:
- no open blocker;
- no open PR that will close it;
- no `needs decision` or `Breaking Change` label.

It assigns each issue to the `gh` user and moves it to *In Progress*. It then fans the batch out
to sub-agents, one worktree and one PR per issue, following [Worktrees](#worktrees).
1 change: 1 addition & 0 deletions CLAUDE.md
Loading