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
21 changes: 16 additions & 5 deletions .agents/prompts/sdk-diagnose-agent-folder-copy.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,27 @@ destination in a consuming repository.
3. Check `EnableAgentFolderInPackage` is not set to `false` anywhere in the build (project file,
`Directory.Build.props`, or command-line `-p:` overrides).
4. Confirm the destination folder: default is `.agents` at the repo root, overridable per-build with
`-p:AgentPackDestinationFolder=<folder>`.
`-p:AgentPackDestinationFolder=<folder>`, and the source defaults to the package-level `.agents`
folder (override with `PurviewAgentFolderSourcePath`).
5. Verify repo-root discovery succeeded: explicit `RepoRoot`, then a nearby `AGENTS.md`, then source-control
root metadata.
6. Re-run the build and confirm the destination folder now contains the copied files (including the
6. If the destination looks stale or incomplete, inspect the change-detection manifest
(`<repo root>/.purview/agent-sync.cache`). It lists the files the SDK believes it already mirrored; a
matching entry with a present destination file means the sync was skipped as up to date. Delete the
manifest (or the affected destination file) to force a fresh copy.
7. Retry notices are demoted to low-importance messages, so a healthy build shows no `MSB3026` warnings.
A copy that still fails after every retry is reported as an error naming the source, destination and
OS error - search the build log for `The Purview SDK could not copy`. Temporarily set
`PurviewAgentFolderCopyRetries` / `PurviewAgentFolderCopyRetryDelayMilliseconds` to retry longer, and
`PurviewSuppressCopyRetryWarnings=false` to see every retry attempt.
8. Re-run the build and confirm the destination folder now contains the copied files (including the
generated `.gitignore` for skill/prompt/agent subfolders).

## Suggested output

- A short root-cause explanation (missing import, disabled flag, wrong destination override, or repo-root
discovery miss).
- A short root-cause explanation (missing import, disabled flag, wrong destination override, repo-root
discovery miss, or a destination held open by another process).
- The exact command used to reproduce/verify the fix (for example
`dotnet build <project> -p:AgentPackDestinationFolder=<folder>`).
- Confirmation that the expected files exist at the resolved destination path.
- Confirmation that the expected files exist at the resolved destination path, plus whether the
manifest skipped the sync (in which case the content was already up to date).
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@ For packable projects, `PurviewAutoSdkPack` (default `true`) automatically adds
- `Sdk/*.md`, `Sdk/*.png`, `Sdk/*.jpg`, etc. → package root
- everything else under `Sdk/` → `Sdk/`

The `BuildSdk.csproj` itself is an MSBuild SDK, so it disables `PurviewAutoSdkPack` and explicitly packs its `Sdk/` contents instead. This is an exception for the SDK project only; every other project that consumes this SDK relies on `PurviewAutoSdkPack` to ship its `Sdk/` folder. Consuming repositories that use this SDK get the bundled agent folder copied into `$(AgentPackDestinationFolder)/` (default `.agents/`) before build when `EnableAgentFolderInPackage` is `true` (default).
The `BuildSdk.csproj` itself is an MSBuild SDK, so it disables `PurviewAutoSdkPack` and explicitly packs its `Sdk/` contents instead. This is an exception for the SDK project only; every other project that consumes this SDK relies on `PurviewAutoSdkPack` to ship its `Sdk/` folder. Consuming repositories that use this SDK get the bundled agent folder mirrored into `$(AgentPackDestinationFolder)/` (default `.agents/`) before build when `EnableAgentFolderInPackage` is `true` (default).

The mirror is change-aware and lock tolerant: `SyncPurviewRepositoryFiles` (inline task in `Sdk/Sdk.targets`) skips unchanged files via the `<repo root>/.purview/agent-sync.cache` manifest, stages every write into a temporary file next to the destination before renaming it into place, treats "another project already wrote identical content" as success, retries quietly, and escalates only after `PurviewAgentFolderCopyRetries` attempts (`PurviewAgentFolderCopyFailureAsError=false` downgrades that to a warning). `PurviewSuppressCopyRetryWarnings` (default `true`) demotes the built-in copy task's retry notice (`MSB3026`) to a message, which is set in `Sdk.targets` so it can be toggled from the project file. The same task performs the `.editorconfig`/`global.json` bootstraps (`PurviewRepoBootstrapMode`: `IfMissing`/`Always`/`WarnOnDrift`/`Never`). `PurviewAgentFolderSourcePath` defaults to the package-level `.agents` folder and is deliberately defined in `Sdk/Sdk.props` — defining it in `Sdk/Props/Defaults.props` resolves `$(MSBuildThisFileDirectory)` to `Sdk/Props/` and silently breaks the packaged layout.

During packaging, the SDK injects a `.gitignore` file into each second-level folder under `Sdk/.agents` with the content `# Ignore all files\n*\n\n# Don't ignore directories, so Git can traverse them\n!*/\n\n# Keep this file\n!.gitignore`, so the copied folder is ignored by Git in consuming repositories while keeping the folder structure discoverable.

Any edit, addition, or deletion in `src/src/BuildSdk/Sdk/.agents/` therefore changes the contents delivered to every repository that consumes this SDK.

Tests for this feature live in `src/tests/BuildSdk.IntegrationTests/AgentPackFolderTests.cs`.
Tests for this feature live in `src/tests/BuildSdk.IntegrationTests/AgentPackFolderTests.cs` (packaging) and `src/tests/BuildSdk.IntegrationTests/RepositoryFileSyncTests.cs` (repository mirroring, change detection, retry and failure behaviour).

## Repository map

Expand Down
34 changes: 31 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,15 @@ The `templates/` folder contains ready-to-copy starter files for new repos:
| `.gitattributes` | Line-ending normalisation for .cs, .json, .yml, etc. |
| `.config/dotnet-tools.json` | CSharpier tool manifest |

The package also ships bundled agent content under `.agents/**`. During build, the SDK copies it into the consuming repository's `.agents/` folder by default so compatible coding agents can discover repository-aware guidance automatically. The SDK also injects a `.gitignore` file into each second-level agent folder with the content `# Ignore all files\n*\n\n# Don't ignore directories, so Git can traverse them\n!*/\n\n# Keep this file\n!.gitignore`, so the copied folder is ignored by Git while keeping the folder structure discoverable.
The package also ships bundled agent content under `.agents/**`. During build, the SDK mirrors it into the consuming repository's `.agents/` folder by default so compatible coding agents can discover repository-aware guidance automatically. The SDK also injects a `.gitignore` file into each second-level agent folder with the content `# Ignore all files\n*\n\n# Don't ignore directories, so Git can traverse them\n!*/\n\n# Keep this file\n!.gitignore`, so the copied folder is ignored by Git while keeping the folder structure discoverable.

The mirror is change-aware: file fingerprints and content hashes are recorded in
`.purview/agent-sync.cache` at the repository root, so unchanged content is skipped and repeat builds
touch no files. Every write is staged into a temporary file and renamed into place, and because all
projects in a solution share these destinations the retries are silent — copy retry notices (`MSB3026`)
are demoted to low-importance messages while a copy that still fails after every retry is reported as an
error. See [Agent Folder](docs/wiki/Agent-Folder.md) and
[Repository Bootstrap](docs/wiki/Repository-Bootstrap.md) for the retry and failure properties.

---

Expand Down Expand Up @@ -272,14 +280,24 @@ Non-packable projects (including web applications) default `WarnOnPackingNonPack
| `BootstrapGlobalJsonToRepoRoot` | `true` | Creates a `global.json` at the repository root when missing. |
| `RepositoryGlobalJsonFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `global.json`. |
| `PurviewBuildSdkVersionForGlobalJson` | *(auto-detected or `1.0.0` fallback)* | Version written to the `msbuild-sdks.Purview.BuildSdk` entry in a bootstrapped `global.json`. |
| `PurviewRepoBootstrapMode` | `IfMissing` | `IfMissing` never touches an existing file, `Always` overwrites it, `WarnOnDrift` reports a file that differs from the SDK-provided one, and `Never` skips bootstrapping. |
| `PurviewRepoBootstrapCopyRetries` | `3` | Write attempts before a bootstrap write failure is reported. |
| `PurviewRepoBootstrapCopyRetryDelayMilliseconds` | `500` | Base delay between bootstrap write attempts. |
| `PurviewRepoBootstrapCopyFailureAsError` | `true` | When `false`, a failed bootstrap write is a warning instead of an error. |
| `PurviewSuppressCopyRetryWarnings` | `true` | Demotes built-in copy retry notices (`MSB3026`) to messages. Set to `false` to see every retry attempt. |

### Agent folder

| Property | Default | Description |
| -- | -- | -- |
| `PurviewAutoSdkPack` | `true` | When `true`, automatically packs the `Sdk/` folder contents into the NuGet package with the correct root-level paths. Disable this for MSBuild SDK projects. |
| `EnableAgentFolderInPackage` | `true` | Copies the bundled `.agents/**` folder from the SDK NuGet package into the consuming repository's `.agents/` folder (or `$(AgentPackDestinationFolder)/`) before build. |
| `AgentPackDestinationFolder` | `.agents` | Repo-relative destination folder that receives the copied agent folder contents when `EnableAgentFolderInPackage` is `true`. |
| `EnableAgentFolderInPackage` | `true` | Mirrors the bundled `.agents/**` folder from the SDK NuGet package into the consuming repository's `.agents/` folder (or `$(AgentPackDestinationFolder)/`) before build. |
| `AgentPackDestinationFolder` | `.agents` | Repo-relative destination folder that receives the mirrored agent folder contents when `EnableAgentFolderInPackage` is `true`. |
| `PurviewAgentFolderSourcePath` | *(package-level `.agents`)* | Overrides the folder that provides the bundled `.agents` content. |
| `PurviewAgentFolderCopyRetries` | `3` | Copy attempts per file before the failure is reported. |
| `PurviewAgentFolderCopyRetryDelayMilliseconds` | `500` | Base delay between copy attempts. |
| `PurviewAgentFolderCopyFailureAsError` | `true` | When `false`, a copy that still fails after every retry is a warning instead of an error. |
| `PurviewAgentSyncManifestPath` | `<repo root>/.purview/agent-sync.cache` | Overrides the change-detection manifest used to skip unchanged agent content. |

To disable bundled agent folder copying in a consuming repo, set the opt-out property before importing the SDK:

Expand Down Expand Up @@ -361,6 +379,16 @@ The SDK now exports its properties via `CompilerVisibleProperty`, so analyzers a
| `BootstrapGlobalJsonToRepoRoot` | When `true` (default), creates `global.json` at `RepositoryGlobalJsonFilePath` if missing. |
| `PurviewBuildSdkVersionForGlobalJson` | Version used for `msbuild-sdks.Purview.BuildSdk` when bootstrapping `global.json` (auto-detected from SDK package path, fallback `1.0.0`). |
| `DisableAutoCopySdkFiles` | When `true`, disables SDK auto-copy/bootstrap for repo files (`.editorconfig`, `global.json`). |
| `PurviewRepoBootstrapMode` | Controls repo file bootstrapping: `IfMissing` (default), `Always`, `WarnOnDrift` or `Never`. |
| `PurviewRepoBootstrapCopyRetries` | Number of write attempts before a bootstrap failure is reported (default `3`). |
| `PurviewRepoBootstrapCopyRetryDelayMilliseconds` | Base delay between bootstrap write attempts (default `500`). |
| `PurviewRepoBootstrapCopyFailureAsError` | When `true` (default), a bootstrap write that still fails after every retry is an error. |
| `PurviewAgentFolderSourcePath` | Folder that provides the bundled `.agents` content mirrored into the repository. |
| `PurviewAgentFolderCopyRetries` | Number of copy attempts per file before an agent folder sync failure is reported (default `3`). |
| `PurviewAgentFolderCopyRetryDelayMilliseconds` | Base delay between agent folder copy attempts (default `500`). |
| `PurviewAgentFolderCopyFailureAsError` | When `true` (default), an agent folder copy that still fails after every retry is an error. |
| `PurviewAgentSyncManifestPath` | Change-detection manifest used to skip unchanged agent content (default `<repo root>/.purview/agent-sync.cache`). |
| `PurviewSuppressCopyRetryWarnings` | When `true` (default), built-in copy retry notices (`MSB3026`) are demoted to messages. |
| `PurviewAutoSdkPack` | When `true`, automatically packs the `Sdk/` folder contents into the NuGet package with the correct root-level paths. |
| `CurrentYear` | Current year used in generated assembly metadata. |
| `AutoGeneratedAssemblyInfoFile` | Relative path to generated AssemblyInfo source file. |
Expand Down
27 changes: 24 additions & 3 deletions docs/wiki/Agent-Folder.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,35 @@ folder by default so compatible coding agents can discover repository-aware guid

## How the copy works

- `EnableAgentFolderInPackage=true` (default) copies the bundled `.agents/**` folder from the SDK
- `EnableAgentFolderInPackage=true` (default) syncs the bundled `.agents/**` folder from the SDK
NuGet package into `$(AgentPackDestinationFolder)/` (default `.agents`) in the consuming repository
**before build**.
- The source folder defaults to the package-level `.agents` folder beside `Sdk/` and can be pointed
elsewhere with `PurviewAgentFolderSourcePath`.
- The destination root is resolved from `RepoRoot` (auto-discovered repo root), an `AGENTS.md` walk-up,
or SourceLink source roots.
- The copy is also performed for bundled `.agents` folders shipped by **any** restored NuGet package
- The sync is also performed for bundled `.agents` folders shipped by **any** restored NuGet package
that used `PurviewAutoSdkPack` (read from `project.assets.json`), not just this SDK.
- `SkipUnchangedFiles=true` keeps repeated builds cheap.
- Unchanged content is skipped using a repository manifest (`.purview/agent-sync.cache` by default), so
repeat builds do not touch any file. The manifest records a per-file size/timestamp fingerprint plus a
content hash, which is what makes an in-place package republish (same version, new content) re-sync.
Deleting a mirrored file also re-triggers its copy.
- Every write is staged into a temporary file in the destination folder and then renamed into place, so a
reader never observes a partially written file and concurrent writers cannot interleave.
- Several projects share the same destination, so simultaneous writes are expected. Those retries are
logged at low importance only (MSB3026 copy retry notices are demoted to messages) and a copy that
still fails after every retry is reported as an **error** by default.

## Failure and retry behaviour

| Property | Default | Description |
| -- | -- | -- |
| `PurviewAgentFolderSourcePath` | *(package-level `.agents`)* | Folder that provides the bundled `.agents` content mirrored into the repository. |
| `PurviewAgentFolderCopyRetries` | `3` | Copy attempts per file before the failure is reported. |
| `PurviewAgentFolderCopyRetryDelayMilliseconds` | `500` | Base delay between attempts (a small increment is added per attempt). |
| `PurviewAgentFolderCopyFailureAsError` | `true` | When `false`, a copy that still fails after every retry is reported as a warning and the build continues. |
| `PurviewAgentSyncManifestPath` | `<repo root>/.purview/agent-sync.cache` | Overrides the change-detection manifest location. |
| `PurviewSuppressCopyRetryWarnings` | `true` | Demotes MSB3026 copy retry notices to messages. Set to `false` to see every retry attempt. |

## Opting out

Expand Down
24 changes: 22 additions & 2 deletions docs/wiki/Configuration-Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,34 @@ operations skip them silently.
| `BootstrapGlobalJsonToRepoRoot` | `true` | Creates a `global.json` at the repository root when missing. |
| `RepositoryGlobalJsonFilePath` | *(auto-detected)* | Override the destination path for the bootstrapped `global.json`. |
| `PurviewBuildSdkVersionForGlobalJson` | *(auto-detected or `1.0.0` fallback)* | Version written to the `msbuild-sdks.Purview.BuildSdk` entry in a bootstrapped `global.json`. |
| `PurviewRepoBootstrapMode` | `IfMissing` | `IfMissing` never touches an existing file, `Always` overwrites it, `WarnOnDrift` reports that an existing file differs from the SDK-provided one, and `Never` skips bootstrapping entirely. |
| `PurviewRepoBootstrapCopyRetries` | `3` | Copy attempts before a bootstrap write failure is reported. |
| `PurviewRepoBootstrapCopyRetryDelayMilliseconds` | `500` | Base delay between bootstrap write attempts. |
| `PurviewRepoBootstrapCopyFailureAsError` | `true` | When `false`, a bootstrap write that still fails after every retry is reported as a warning and the build continues. |
| `PurviewSuppressCopyRetryWarnings` | `true` | Demotes MSB3026 copy retry notices to messages. Set to `false` to see every retry attempt. |

Bootstrap writes are staged into a temporary file and renamed into place, so editors and tools never
observe a partially written `.editorconfig` or `global.json`. Because every project in a solution runs
the same bootstrapping targets, a lost race between parallel projects is a no-op: an existing file is
treated as success rather than a copy failure.

## Agent folder

| Property | Default | Description |
| -- | -- | -- |
| `PurviewAutoSdkPack` | `true` | When `true`, automatically packs the `Sdk/` folder contents into the NuGet package with the correct root-level paths. Disable this for MSBuild SDK projects. |
| `EnableAgentFolderInPackage` | `true` | Copies the bundled `.agents/**` folder from the SDK NuGet package into the consuming repository's `.agents/` folder (or `$(AgentPackDestinationFolder)/`) before build. |
| `AgentPackDestinationFolder` | `.agents` | Repo-relative destination folder that receives the copied agent folder contents when `EnableAgentFolderInPackage` is `true`. |
| `EnableAgentFolderInPackage` | `true` | Mirrors the bundled `.agents/**` folder from the SDK NuGet package into the consuming repository's `.agents/` folder (or `$(AgentPackDestinationFolder)/`) before build. |
| `AgentPackDestinationFolder` | `.agents` | Repo-relative destination folder that receives the mirrored agent folder contents when `EnableAgentFolderInPackage` is `true`. |
| `PurviewAgentFolderSourcePath` | *(package-level `.agents`)* | Overrides the folder that provides the bundled `.agents` content. |
| `PurviewAgentFolderCopyRetries` | `3` | Copy attempts per file before the failure is reported. |
| `PurviewAgentFolderCopyRetryDelayMilliseconds` | `500` | Base delay between copy attempts. |
| `PurviewAgentFolderCopyFailureAsError` | `true` | When `false`, a copy that still fails after every retry is reported as a warning and the build continues. |
| `PurviewAgentSyncManifestPath` | `<repo root>/.purview/agent-sync.cache` | Overrides the change-detection manifest used to skip unchanged agent content. |

The mirror is change-aware: content that already matches the manifest is skipped entirely, which keeps
repeat builds free of file writes and file locks. Retry notices are demoted to low-importance messages
(`MSB3026`); a copy that still fails after every retry is reported as an error that names the source,
destination and OS error.

To disable bundled agent folder copying in a consuming repo, set the opt-out property before
importing the SDK:
Expand Down
Loading
Loading