From 2f9b1ce177f44af0223a46a7a870677f28413361 Mon Sep 17 00:00:00 2001 From: Paul Trampert Date: Thu, 1 Oct 2026 10:56:22 -0400 Subject: [PATCH 1/3] (PATCH): Add AGENTS.md with CLAUDE.md symlinked to it Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + 2 files changed, 79 insertions(+) create mode 100644 AGENTS.md create mode 120000 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1209545 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,78 @@ +# AGENTS.md + +Guidance for coding agents working in this repository. `CLAUDE.md` is a symlink to this file. + +## 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`, and the library builds the concrete class at runtime. + +## Layout + +| Project | Target | Purpose | +| --- | --- | --- | +| `PTrampert.SimplePatch` | net8.0 | Core package: `Optional`, `IPatchObject`, `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` 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. +- Git worktrees go under `.claude/worktrees/`, which git ignores. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file From c7a726a815cb4c15f34e6d0af06d4c3746e2929b Mon Sep 17 00:00:00 2001 From: Paul Trampert Date: Thu, 1 Oct 2026 11:07:29 -0400 Subject: [PATCH 2/3] (PATCH): Add the /implement-unblocked command Adapted from PacmanManager. It also skips issues labelled Breaking Change, which must wait for a major version, and drops PacmanManager's E2E and claude-autofix steps, which don't apply here. AGENTS.md gains the worktree rules the command relies on. Co-Authored-By: Claude Opus 5.5 --- .claude/commands/implement-unblocked.md | 193 ++++++++++++++++++++++++ AGENTS.md | 38 ++++- 2 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 .claude/commands/implement-unblocked.md diff --git a/.claude/commands/implement-unblocked.md b/.claude/commands/implement-unblocked.md new file mode 100644 index 0000000..5f273fa --- /dev/null +++ b/.claude/commands/implement-unblocked.md @@ -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 --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= + ``` + + Then set the option whose name is `In Progress`, compared case-insensitively: + + ```bash + gh project item-edit --id --project-id \ + --field-id --single-select-option-id