{"owner":"dotnet","repo":"msbuild","hasSkills":true,"totalSkillsCount":25,"totalTokensCount":51061,"categories":["copilot-instructions","marketplace","anthropic-skill","root-instruction","plugin-manifest"],"hasMcp":false,"mcpConfig":null,"found":[".github/copilot-instructions.md",".github/plugin/marketplace.json",".github/skills/assessing-breaking-changes/SKILL.md",".github/skills/authoring-errors-and-warnings/SKILL.md",".github/skills/changewaves/SKILL.md",".github/skills/cswin32-com/SKILL.md",".github/skills/cswin32-interop/SKILL.md",".github/skills/deploy-msbuild-to-vs/SKILL.md",".github/skills/dotnet-aot-compat/SKILL.md",".github/skills/dotnet-aot-compat/references/polyfills.md",".github/skills/integrating-sdk-and-msbuild/SKILL.md",".github/skills/maintaining-binary-log-compatibility/SKILL.md",".github/skills/merge-dependency-updates/SKILL.md",".github/skills/optimizing-msbuild-performance/SKILL.md",".github/skills/pipelines-health-check/SKILL.md",".github/skills/project-management/SKILL.md",".github/skills/release/SKILL.md",".github/skills/reviewing-msbuild-code/SKILL.md",".github/skills/running-unit-tests/SKILL.md",".github/skills/use-bootstrap-msbuild/SKILL.md","AGENTS.md","plugins/mt-migration/README.md","plugins/mt-migration/plugin.json","plugins/mt-migration/skills/multithreaded-task-migration/SKILL.md","src/StringTools/README.md"],"skills":{".github/copilot-instructions.md":"../AGENTS.md",".github/plugin/marketplace.json":"{\n  \"name\": \"msbuild-plugins\",\n  \"owner\": {\n    \"name\": \"dotnet/msbuild\"\n  },\n  \"metadata\": {\n    \"description\": \"GitHub Copilot plugins for the MSBuild ecosystem\",\n    \"version\": \"1.0.0\"\n  },\n  \"plugins\": [\n    {\n      \"name\": \"mt-migration\",\n      \"description\": \"MSBuild multithreaded task migration toolkit: the migration playbook + an MT-specific code reviewer that traces call chains end-to-end.\",\n      \"version\": \"0.1.0\",\n      \"source\": \"./plugins/mt-migration\"\n    }\n  ]\n}\n",".github/skills/assessing-breaking-changes/SKILL.md":"---\nname: assessing-breaking-changes\ndescription: 'Guides assessment of backward compatibility for MSBuild changes. Consult when modifying behavior, adding warnings or errors, changing defaults, altering target ordering, removing or deprecating features, deciding whether a change needs a ChangeWave, reviewing blast radius of behavioral changes, or when a PR introduces user-visible output differences.'\nargument-hint: 'Describe the change and its potential compatibility impact.'\n---\n\n# Backward Compatibility in MSBuild\n\nBackward compatibility is the default — any change that could alter existing build behavior must be explicitly justified.\n\nThis skill covers **how to evaluate compatibility risk**. For the mechanics of ChangeWave implementation, see the [changewaves skill](../changewaves/SKILL.md).\n\n## Core Philosophy\n\n1. **Existing builds must not break.** If a project built successfully yesterday, it must build successfully today with identical semantics.\n2. **New warnings are breaking changes.** Builds that use `-WarnAsError` or `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` will fail if you introduce a new warning. Always gate new warnings behind a ChangeWave or emit them as `Message` importance instead.\n3. **Output changes are behavioral changes.** Even \"improvements\" to output formatting, file paths, or diagnostic text can break downstream consumers that parse MSBuild output.\n4. **Removal is nearly impossible.** Never remove CLI switches, public APIs, or property names. Deprecate with warnings first, then gate removal behind a ChangeWave after multiple release cycles.\n\n## Blast Radius Checklist\n\nBefore merging any behavioral change, evaluate:\n\n| Question | If Yes |\n|----------|--------|\n| Does this change what gets built or how? | ChangeWave required |\n| Does this add a new warning? | ChangeWave required (WarnAsError impact) |\n| Does this change a property default? | ChangeWave required (existing .csproj files depend on defaults) |\n| Does this alter target execution order? | Test with real-world solutions; likely ChangeWave |\n| Does this change console or binlog output format? | Consider downstream tool impact |\n| Does this affect only internal code paths with no user-visible effect? | No ChangeWave needed |\n| Is this a pure bug fix restoring documented behavior? | Usually no ChangeWave; use judgment on blast radius |\n\n## When ChangeWave Is NOT Needed\n\n- Internal refactoring with no observable behavior change\n- Performance improvements that don't change semantics\n- New opt-in features gated by a new property or switch\n- Bug fixes that restore clearly-intended behavior with limited blast radius\n\nFor detailed ChangeWave mechanics, see [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md) and [ChangeWaves.md](../../../documentation/wiki/ChangeWaves.md).\n\n## The Warnings-as-Errors Rule\n\nThis is the most commonly missed compatibility concern:\n\n```xml\n<!-- Many enterprise builds set this globally -->\n<TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n```\n\nAny new `MSBxxxx` warning you add will **break these builds**. Options:\n1. **Gate behind ChangeWave** — preferred for genuinely important warnings\n2. **Use `MessageImportance.Low` or `Normal`** — for informational diagnostics\n3. **Add as an error from the start** — if the condition is always wrong\n\n## Deprecation Protocol\n\n1. Add a deprecation warning (behind ChangeWave if broad impact)\n2. Document the deprecation in release notes\n3. Maintain the old behavior for at least two major .NET versions\n4. Only remove after the ChangeWave has rotated out\n\n## Compatibility Test Matrix\n\nWhen testing backward compatibility, verify:\n\n- **Multi-targeting projects** — `<TargetFrameworks>net472;net8.0</TargetFrameworks>`\n- **Solution builds with mixed project types** — C#, F#, VB, C++/CLI\n- **Incremental builds** — second build should be a no-op\n- **Design-time builds** — Visual Studio calls different target contracts\n- **Cross-platform** — path separator differences, case sensitivity on Linux\n- **WarnAsError builds** — explicitly test with `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>`\n\n## Decision Framework\n\n```\nIs the change user-visible?\n├── No → Ship it (no ChangeWave needed)\n└── Yes\n    ├── Is it a new opt-in feature? → Ship it (no ChangeWave needed)\n    └── Does it change existing behavior?\n        ├── Bug fix with low blast radius? → Ship it, add regression test\n        └── Behavioral change or new warning?\n            └── Gate behind ChangeWave, test opt-out path\n```\n\nDocument the compatibility decision in your PR description so reviewers can validate it.\n",".github/skills/authoring-errors-and-warnings/SKILL.md":"---\nname: authoring-errors-and-warnings\ndescription: 'Guides authoring of MSBuild errors, warnings, and diagnostic messages. Consult when adding new MSBxxxx codes, writing or modifying user-facing diagnostic text, deciding between error/warning/message severity, working with Strings.resx resource files, formatting paths in error output, or evaluating whether a new warning could break WarnAsError builds.'\nargument-hint: 'Describe the error/warning being added or modified.'\n---\n\n# Error and Warning Authoring in MSBuild\n\nError messages are MSBuild's primary user interface. They must help developers fix problems without reading source code.\n\nFor the mechanics of error code assignment, see [assigning-msb-error-code.md](../../../documentation/assigning-msb-error-code.md).\n\n## Error Message Quality Rules\n\nEvery error message must answer three questions:\n\n1. **What happened?** — State the problem clearly\n2. **Why?** — Provide context (file, property, expected value)\n3. **What should the user do?** — Give actionable guidance\n\n```xml\n<!-- BAD: What is \"it\"? What should I do? -->\n<value>MSB4999: Invalid configuration.</value>\n\n<!-- GOOD: States the problem, context, and fix -->\n<value>MSB4999: The project \"{0}\" specifies TargetFramework \"{1}\" which is not installed. Install the SDK for \"{1}\" or update the TargetFramework in the project file.</value>\n```\n\n## Severity Decision Framework\n\n```\nIs the condition always wrong (invalid input, impossible state)?\n├── Yes → ERROR (build should fail)\n└── No\n    ├── Could this cause subtle build correctness issues?\n    │   ├── Yes, likely → WARNING (but see WarnAsError impact below)\n    │   └── Yes, maybe → MESSAGE at Normal importance\n    └── Is this purely informational?\n        └── Yes → MESSAGE at Low importance\n```\n\n### The WarnAsError Constraint\n\n**New warnings are breaking changes** for builds using:\n- `-WarnAsError` (CLI)\n- `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` (project)\n- `<WarningsAsErrors>MSBxxxx</WarningsAsErrors>` (specific codes)\n\nBefore adding a new warning, consider:\n1. **Is the warning important enough to justify breaking WarnAsError builds?** If yes, gate behind a ChangeWave (see [changewaves skill](../changewaves/SKILL.md)).\n2. **Could this be a Message instead?** Messages never break builds but still appear in binary logs.\n3. **Should this be an error from the start?** If the condition is always wrong, skip the warning stage.\n\n## MSB Error Code Assignment\n\n### Code Ranges\n\n| Range | Area |\n|-------|------|\n| `MSB1xxx` | Command-line handling |\n| `MSB3xxx` | Tasks (`Microsoft.Build.Tasks.Core.dll`) |\n| `MSB4xxx` | Engine (`Microsoft.Build.dll`) |\n| `MSB5xxx` | Shared code across assemblies |\n| `MSB6xxx` | Utilities (`Microsoft.Build.Utilities`) |\n\n### Assignment Process\n\n1. Open the `Strings.resx` file for the appropriate assembly\n2. Find the \"Next message code\" comment at the bottom of the file\n3. Search the repo to confirm the code is not already in use\n4. Use the code; update the comment to the next available number\n\nSee [assigning-msb-error-code.md](../../../documentation/assigning-msb-error-code.md) for detailed steps.\n\n## Resource String Format\n\n```xml\n<data name=\"FeatureArea.DescriptiveName\">\n  <value>MSBxxxx: Clear description with {0} placeholders for runtime values.</value>\n  <comment>{StrBegin=\"MSBxxxx: \"}{0} is the project file path. {1} is the property name.</comment>\n</data>\n```\n\n### Rules\n\n- Resource name uses `FeatureArea.DescriptiveName` convention\n- Value starts with `MSBxxxx: ` (code, colon, space)\n- Comment documents the `{StrBegin}` marker and explains each `{N}` placeholder\n- Comments guide translators — mention untranslatable tokens\n\n## Consuming Error Resources in Code\n\nUse the standard formatting method that extracts and applies the error code. Engine-level project\nerrors use `Microsoft.Build.Internal.ProjectErrorUtilities`:\n\n```csharp\n// For errors\nLog.LogErrorWithCodeFromResources(\"Copy.Error\", sourceFile, destFile, ex.Message);\n\n// For warnings\nLog.LogWarningWithCodeFromResources(\"ResolveAssemblyReference.Conflict\", assemblyName);\n\n// For engine-level errors (not in tasks)\nProjectErrorUtilities.ThrowInvalidProject(\n    elementLocation,\n    \"InvalidProjectFile\",\n    arg1, arg2);\n```\n\n**Never** construct error messages by string concatenation — always use resource strings for localization support.\n\n## Localization Requirements\n\n- Error **codes** (`MSBxxxx`) are never localized\n- Error **text** is localized into all supported languages\n- After adding/modifying resource strings, run a full build to generate `.xlf` placeholder translations\n- Use `<comment>` elements to help translators understand context\n- See [Localization.md](../../../documentation/wiki/Localization.md) for the full localization process\n\n## Path Formatting in Error Messages\n\n- Show paths relative to the project directory when possible\n- Use the path exactly as the user specified it (don't normalize slashes)\n- Include file and line number via `IElementLocation` when available — this enables IDE click-to-navigate\n- For paths that could contain spaces, ensure they're quoted in the message\n\n## Checklist for New Errors/Warnings\n\n- [ ] Error code assigned from correct `Strings.resx` range\n- [ ] \"Next message code\" comment updated in the resx\n- [ ] Message answers: what happened, why, what to do\n- [ ] `{StrBegin}` comment and placeholder documentation added\n- [ ] Severity chosen (error vs warning vs message) with WarnAsError considered\n- [ ] If warning: ChangeWave gating evaluated\n- [ ] Full build run to generate `.xlf` files\n- [ ] Test verifies the error is produced with correct code and text\n",".github/skills/changewaves/SKILL.md":"---\nname: changewaves\ndescription: 'Manage MSBuild Change Waves: create new waves, condition features behind opt-out flags, write tests for wave-gated features, document change waves in ChangeWaves.md, and retire expired waves. Use when adding changes that need an opt-out or rotating out old change waves. Changes that introduce a user-visible behavior change should consider whether to use a changewave.'\nargument-hint: 'Add, query, or remove changewaves and changewave checks.'\n---\n\n# Managing MSBuild Change Waves\n\nA Change Wave is an opt-out flag that groups risky features together. Users disable features by setting the environment variable `MSBUILDDISABLEFEATURESFROMVERSION` to the wave version. This skill covers the **how** — the full lifecycle: creating a wave, conditioning code on it, testing, documenting, and retiring.\n\nFor the **when** — deciding whether a change is a breaking change and whether it needs a ChangeWave at all — see [assessing-breaking-changes](../assessing-breaking-changes/SKILL.md).\n\n\n## Decide Whether a Change Wave Is Appropriate\n\nUse a change wave when a change is valuable but has meaningful compatibility risk for existing builds.\n\nGood candidates:\n- User-visible behavior changes that may regress some build graphs.\n- Changes in parsing/evaluation/execution semantics where real-world usage is broad or hard to predict.\n- Changes that may require customer adaptation time and benefit from a temporary opt-out.\n\nUsually avoid a change wave for:\n- Pure bug fixes that restore intended existing behavior with low compatibility risk.\n- Internal refactoring/perf work with no externally observable behavior change.\n- New functionality that is already explicitly opt-in (for example, gated by a new property, switch, or API surface).\n\nIf uncertain, default to safety:\n1. Assess blast radius and rollback difficulty.\n2. Check whether the change could be experienced as a breaking change in production builds.\n3. Use a wave if an opt-out is likely needed while customers adapt.\n\nDocument the decision in PR notes so reviewers can validate the call.\n\nIf a changewave is appropriate, consult [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md) (developer-facing) and [ChangeWaves.md](../../../documentation/wiki/ChangeWaves.md) (public-facing) for details on how to follow the below steps.\n\n## Step 1: Determine the Correct Wave Version\n\nLook up the current MSBuild version. If a wave already exists for that version in `src/Framework/ChangeWaves.cs`, use it. Otherwise, create a new one.\n\n## Step 2: Condition Your Feature on the Wave\n\nC# and MSBuild examples are in [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md). Prefer to put checks inline rather than abstracting out a method to host the check.\n\n## Step 3: Write Tests\n\nWrite normal tests for the new behavior. Then add at least one test that verifies the old behavior is **preserved** when the wave is opted out.\n\n## Step 4: Document the Feature\n\nAdd an entry to the **Current Rotation of Change Waves** section in [ChangeWaves.md](../../../documentation/wiki/ChangeWaves.md).\n\n- If a heading for this wave already exists, add a bullet under it.\n- If not, add a new `### {Major}.{Minor}` heading at the top of the current rotation list.\n\nEach entry is a bullet with a link to the PR:\n\n```markdown\n### 18.6\n- [Short description of the change.](https://github.com/dotnet/msbuild/pull/NNNNN)\n```\n\n## Only when explicitly requested: Retire an Expired Wave\n\nWaves rotate out when a new major .NET version will be released. When retiring a wave, follow the detailed description in [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md).\n\n## Checklist\n\n- [ ] Decision made: change wave needed?\n- [ ] Correct wave version chosen from `eng/Versions.props`\n- [ ] Wave field and `AllWaves` entry added in `ChangeWaves.cs` (if new wave)\n- [ ] Feature code wrapped with `AreFeaturesEnabled`\n- [ ] Test verifying opt-out disables the feature\n- [ ] Entry added to `documentation/wiki/ChangeWaves.md`\n",".github/skills/cswin32-com/SKILL.md":"---\nname: cswin32-com\ndescription: 'Guides struct-based COM interop in MSBuild using CsWin32 patterns. Consult when working with the generic ComScope type, ComClassFactory, IComIID, the IID.Get generic method, delegate* unmanaged vtables, CoCreateInstance, or manually defining COM interfaces not in Win32 metadata (e.g. WMI IWbemLocator, IWbemServices).'\nargument-hint: 'Describe the COM interface or activation pattern you are working with.'\n---\n\n# CsWin32 COM Interop Guide\n\nStruct-based COM interop using CsWin32 patterns — AOT-compatible, no `[ComImport]` or built-in marshalling.\n\n**Paired skill:** [cswin32-interop](../cswin32-interop/SKILL.md) covers general P/Invoke and the blittable signature rules that apply to both `[DllImport]` and COM vtables. This file covers only the COM-specific layer.\n\n## Workflow\n\n1. **Interface in Win32 metadata?** Add the name to `src/Framework/NativeMethods.txt` → CsWin32 generates it. Not in metadata (WMI, Fusion, Setup Configuration) → define a manual struct under its own folder, excluded from source builds via `<Compile Remove>`.\n2. **Lifetime: `using ComScope<T> scope = new();`** for every transient COM pointer (`CoCreateInstance`, `QueryInterface`, `IEnumXxx::Next`, factory output, app-local `STDAPI Get*`, etc.). Never write `T* local; try { ... } finally { local->Release(); }` — that's the pre-`ComScope` shape that leaks on every early return. Same for `BSTR` out-params: `using BSTR x = default;`.\n3. **Activate** via `ComClassFactory.TryCreate(CLSID, ...)` (AOT-compatible) or `PInvoke.CoCreateInstance` with `IID.Get<T>()` — not `&localGuid`.\n4. **Call** via `scope.Pointer->Method(...)`. Pass `ComScope<T>` directly where the API expects `T**` / `void**` — the implicit operator does the address-of.\n5. **Match the caller's error contract.** If the top-level consumer swallows COM failure (\"no result\" == \"absent\"), helpers return `default` / `false` instead of throwing a `COMException` that's immediately discarded. `ThrowOnFailure` only when the exception will actually propagate or assert a should-never-happen.\n6. **Guard with `#if FEATURE_WINDOWSINTEROP`** — add `&& NET` only when the struct uses the static-abstract `IComIID` form exclusively (no net472 dual-target).\n\n## Manual COM Structs (Not in Metadata)\n\nDefine each interface in its own file under e.g. `src/Tasks/AssemblyDependency/Fusion/`, `src/Framework/Shared/VisualStudio/`, `src/Framework/Utilities/Wmi/`. Exclude from source builds. Pattern (dual-target — works on net472 + .NET):\n\n```csharp\ninternal unsafe struct IAssemblyCache : IComIID\n{\n    public static readonly Guid IID_IAssemblyCache = new(0xE707DCDE, ...);\n\n#if NET\n    static ref readonly Guid IComIID.Guid\n    {\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        get => ref Unsafe.AsRef(in IID_IAssemblyCache);\n    }\n#else\n    readonly ref readonly Guid IComIID.Guid\n    {\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        get => ref Unsafe.AsRef(in IID_IAssemblyCache);\n    }\n#endif\n\n    private readonly void** _lpVtbl;\n\n    // IUnknown at indices 0-2, then interface methods at 3+\n    public HRESULT QueryInterface(Guid* riid, void** ppvObject)\n    {\n        fixed (IAssemblyCache* pThis = &this)\n            return ((delegate* unmanaged[Stdcall]<IAssemblyCache*, Guid*, void**, HRESULT>)_lpVtbl[0])(pThis, riid, ppvObject);\n    }\n    // AddRef @1, Release @2, then interface methods at correct slot indices.\n}\n```\n\n**Rules:**\n- `delegate* unmanaged[Stdcall]` for the function-pointer cast — IDL `STDMETHODCALLTYPE` ⇒ `__stdcall` on Win32. Picking the wrong convention silently corrupts the stack.\n- `IComIID` has two shapes: static-abstract (`static ref readonly Guid IComIID.Guid`) on .NET 7+, instance (`readonly ref readonly Guid IComIID.Guid`) on net472 / netstandard2.0. CsWin32 picks the right one for its generated structs automatically (see [IComIID across TFMs](#icomiid-across-tfms)); a **manual** struct must spell out both arms itself. For .NET-only structs (WMI), drop the `#else` branch and gate the whole file `#if NET`. For dual-target structs (Fusion), keep both as shown — see `src/Tasks/AssemblyDependency/Fusion/` for the canonical example.\n- Use CsWin32-generated `PCWSTR` / `PWSTR` for wide strings (add to `NativeMethods.txt`); raw `char*` only when no typed equivalent exists.\n- Vtable slots are exact: index 0 = `QueryInterface`, 1 = `AddRef`, 2 = `Release`, 3+ = interface methods in IDL order. When inheriting, **add the parent interface's method count** before counting your own (e.g. `ISetupConfiguration2.EnumAllInstances` is at slot 6 = 3 IUnknown + 3 v1 + 0).\n- CS0592 prevents `[SupportedOSPlatform]` on structs — put it on individual methods.\n\n## Lifetime & Access\n\n**A raw `T*` (COM struct) is forbidden as a field on a non-`ref` type.** Allowed locations: locals in `unsafe` methods, parameters, fields of a `ref struct`. Anywhere else (instance fields of `class` / non-`ref` `struct`) → use `AgileComPointer<T>` — a raw pointer field is an apartment-agility hazard and leaks on finalize-without-Dispose.\n\n### `ComScope<T>` — transient pointers\n\n`ref struct`, `using`'d, calls `Release` on dispose. **The default for everything that doesn't survive the current method.** Implicitly converts to `T**` and `void**`, so out-params write into the scope directly:\n\n```csharp\nusing ComScope<ISomeInterface> scope = new();\nGuid clsid = SomeStruct.CLSID;\nGuid iid = IID.Get<ISomeInterface>();\nPInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &iid, scope).ThrowOnFailure();\nscope.Pointer->DoThing(...);\n\nusing ComScope<IOther> other = new();\nGuid otherIid = IOther.IID_IOther;\nscope.Pointer->QueryInterface(&otherIid, other).ThrowOnFailure();\n```\n\n- Access methods via `scope.Pointer->Method(...)`. Null-check with `scope.IsNull`.\n- Applies to *every* COM out-param: `CoCreateInstance`, `QueryInterface`, `IEnumXxx::Next`, factory methods, app-local `STDAPI Get*` (declare the `[DllImport]` with `T** pp`, not `out T*`, so the implicit operator binds).\n\n**Ownership transfer out of a helper.** A helper that acquires a COM pointer can return `ComScope<T>` directly; intermediate pointers stay in their own `using` scopes and Release on the helper's `return`. `default` `ComScope<T>` is null and its `Dispose` is a no-op, so callers don't need an extra null guard:\n\n```csharp\nprivate static ComScope<ISetupConfiguration2> AcquireSetupConfig()\n{\n    Guid clsid = SetupConfiguration.CLSID_SetupConfiguration;\n    Guid iid = ISetupConfiguration2.IID_ISetupConfiguration2;\n\n    ComScope<ISetupConfiguration2> config = new();\n    HRESULT hr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &iid, config);\n    if (hr.Succeeded) return config;                 // ownership flows to caller\n    if (hr != HRESULT.REGDB_E_CLASSNOTREG) return default;\n\n    using ComScope<ISetupConfiguration> v1 = new();  // intermediate, Released on return\n    if (SetupConfiguration.GetSetupConfiguration(v1, IntPtr.Zero) < 0 || v1.IsNull) return default;\n    return v1.Pointer->QueryInterface(&iid, config).Succeeded ? config : default;\n}\n\n// caller:\nusing ComScope<ISetupConfiguration2> config = AcquireSetupConfig();\nif (!config.IsNull) { /* use config.Pointer */ }\n```\n\n### `BSTR` — COM strings\n\nCsWin32-generated; extended in [`Windows/Win32/Foundation/BSTR.cs`](../../../src/Framework/Windows/Win32/Foundation/BSTR.cs) with `IDisposable`. `Dispose` calls `SysFreeString` and zeroes the field in place. **Scope every COM `BSTR` out-param with `using BSTR x = default;`** — no manual `try/finally + SysFreeString`:\n\n```csharp\nusing BSTR versionBstr = default;\nif (instance->GetInstallationVersion(&versionBstr).Failed) return false;\nstring version = versionBstr.ToString();\n// SysFreeString runs at scope exit.\n```\n\nIn-place-only: `BSTR.Dispose` writes through `Unsafe.AsRef(in this)`, so `using` must be on the storage location, not on a method-returned copy. Same rule as `ComScope<T>`.\n\n### `AgileComPointer<T>` — fields outliving a method\n\nFinalizable managed class. Use whenever a COM pointer is stored in a class field (the only legal way — raw `T*` fields are forbidden, see top of section).\n\n- Registers in the Global Interface Table (thread-agile); finalizer releases if `Dispose` is missed.\n- Access via `using ComScope<T> scope = agile.GetInterface();` then `scope.Pointer->Method(...)`. `GetInterface()` round-trips through the GIT — hoist a single scope to the top of a method when several calls share it.\n- Constructor `takeOwnership`: pass `false` when the raw pointer is also held by a `ComScope<T>` that will Release (GIT AddRefs independently); pass `true` only when no other code path will Release.\n- Dispose via the owner's `Dispose` / `DisposeManagedResources` (`AgileComPointer` is managed, not unmanaged — its finalizer is the safety net, not the primary cleanup).\n\n## Activation\n\n```csharp\n// AOT-compatible\nif (ComClassFactory.TryCreate(IWbemLocator.CLSID, out var factory, out HRESULT hr))\n    using ComScope<IWbemLocator> instance = factory.TryCreateInstance<IWbemLocator>(out hr);\n\n// CoCreateInstance — IID.Get<T>(), not &localGuid\nGuid clsid = IWbemLocator.CLSID;\nusing ComScope<IWbemLocator> locator = new();\nhr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.Get<IWbemLocator>(), locator);\n```\n\n## Error-Handling Parity When Migrating\n\nStruct-based COM returns raw `HRESULT`; `[ComImport]` and built-in activation threw automatically. Preserve the old throw-vs-return at each call site:\n\n| Old shape | Threw via | Migrated shape |\n|---|---|---|\n| `new SomeCoClass()` | built-in activation | `PInvoke.CoCreateInstance(...).ThrowOnFailure()` |\n| `(IFoo)rcw` cast | `InvalidCastException` on QI | `QueryInterface(&iid, scope).ThrowOnFailure()` |\n| `[ComImport]` method, default `PreserveSig=false` | marshaller throws on `FAILED(hr)` | `.ThrowOnFailure()` at the call site |\n| `[ComImport]` method with `[PreserveSig]` | caller inspects return | mirror the existing `hr` branch — don't start throwing |\n\n**Factory-method exception:** when the old contract was \"return null for invalid input\" (e.g. `Create(path)`), keep null-return only for the legitimate-rejection path; activation / QI failures still throw. See [`MetadataReader.cs`](../../../src/Tasks/ManifestUtil/MetadataReader.cs) (throws on activation/QI, returns null on `OpenScope`).\n\n**Don't throw when the caller swallows it.** If the top-level consumer wraps the whole chain in `catch (COMException) { }`, inner helpers must return `default` / `false` / empty instead of constructing a `COMException` only to have it discarded. Throwing here allocates the exception, walks the stack, and hides the HRESULT for no benefit. See [`VisualStudioLocationHelper.AcquireSetupConfiguration2`](../../../src/Framework/VisualStudioLocationHelper.cs).\n\n## Mocking Struct-Based COM in Tests\n\nStruct-based COM calls go through raw vtable pointers, so a managed mock can't be passed where a `T*` is expected. Bridge with the **built-in COM marshaller**: the mock implements the *managed* interface (BCL `System.Runtime.InteropServices.ComTypes.*` when ABI-compatible — true for `ITypeLib` / `ITypeInfo` — else CsWin32's nested `[ComImport] internal interface Interface`), and the test passes a **CCW** (COM-callable wrapper) pointer to the code under test:\n\n```csharp\n// MockTypeLib : ComTypes.ITypeLib   (a normal managed class)\nIntPtr ccw = Marshal.GetComInterfaceForObject(mock, typeof(ComTypes.ITypeLib));\ntry { walker.AnalyzeTypeLibrary((ITypeLib*)ccw); }   // struct API; real QueryInterface works\nfinally { Marshal.Release(ccw); }\n```\n\nThe CCW exposes a real vtable, so no hand-rolled native vtable is needed and the managed mock framework stays intact. But the mock must now behave like real COM, not a managed object:\n\n- **All `[out]` params are written**, even ones the caller ignores. Passing `null` for a trailing out-param (`GetDocumentation(-1, &name, null, null, null)`) faults the marshaller — supply a real pointer for each.\n- **Exception identity is lost.** An HRESULT-returning method resurfaces a thrown exception as a *new* `COMException` (HRESULT preserved, instance not); a `uint`/`void` method with no HRESULT channel (`GetTypeInfoCount`, the `Release*` methods) **swallows it silently** and returns garbage. Assert on type/count, not the injected instance, and don't rely on `void`/`uint` methods to propagate.\n- **Throw `COMException` on bad input, don't `Assert`.** An `Assert` throws an `XunitException` that escapes the SUT's `catch (COMException)`; a `COMException` becomes a failing HRESULT the SUT handles normally. (Matters because a swallowed `GetTypeInfoCount` failure can feed an out-of-range index to the next call.)\n\nSee [`MockTypeLib.cs`](../../../src/Tasks.UnitTests/MockTypeLib.cs) / [`MockTypeInfo.cs`](../../../src/Tasks.UnitTests/MockTypeInfo.cs) and the CCW bridge in [`ComReferenceWalker_Tests.cs`](../../../src/Tasks.UnitTests/ComReferenceWalker_Tests.cs).\n\n## Strongly-typed handle / token wrappers\n\nWhen the native side `typedef`s a primitive into a family of \"same shape, different meaning\" aliases (`corhdr.h`'s `typedef ULONG32 mdToken; typedef mdToken mdAssembly; ...`), mirror that hierarchy with distinct `readonly struct` wrappers holding the single underlying primitive — same pattern as CsWin32's `HANDLE` / `HWND` / `HMODULE`. Stays blittable; `delegate*` casts and arrays marshal at zero cost.\n\nConversions follow the typedef hierarchy: **implicit** widening to the base (`MdAssembly` → `MdToken`, always safe), **explicit** narrowing (`(MdAssembly)token`, opt-in because the C side can't enforce the kind at the cast site).\n\n**Check the native header for the canonical validation primitives** before defining `IsNil` / `IsValid`. Encoding details often hide in macros that don't grep cleanly. For `mdToken` the encoding is `(TableType << 24) | Rid`:\n\n| C macro | Meaning |\n|---|---|\n| `TypeFromToken(tk) = tk & 0xff000000` | Table-type tag (high byte) → `CorTokenType` |\n| `RidFromToken(tk) = tk & 0x00ffffff`  | Row id (low 24 bits) |\n| `IsNilToken(tk) = RidFromToken(tk) == 0` | Nil check — **rid half, not the whole value** |\n| `mdAssemblyNil = mdtAssembly = 0x20000000` | Per-type nil is the table-type tag, **not 0** |\n\nA naive `IsNil => Value == 0` would misclassify the `0x20000000` \"no assembly in this scope\" return from `GetAssemblyFromScope`. Mirror the macro: `IsNil => Rid == 0`. Reference: [`Tokens.cs`](../../../src/Tasks/AssemblyDependency/Metadata/Tokens.cs), [`CorTokenType.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorTokenType.cs).\n\n## IComIID across TFMs\n\nCsWin32 (since **0.3.287**) emits `IComIID` and attaches it to every generated COM struct on **all** target frameworks — static-abstract `static ref readonly Guid Guid { get; }` on .NET 7+, instance `ref readonly Guid Guid { get; }` on net472 / netstandard2.0. No hand-authored polyfill is needed; the old `src/Framework/Polyfills/IComIID.cs` + `IComIIDPolyfills.cs` files (one `partial` per generated struct) are gone.\n\nWhen you add a new CsWin32-generated COM type to `NativeMethods.txt`, it carries `IComIID` automatically through `ComScope<T>` — nothing extra to do.\n\nOnly **manual** structs (WMI, Fusion, Setup Configuration, CLR metadata) implement `IComIID` by hand, matching the per-TFM shape — see [Manual COM Structs](#manual-com-structs-not-in-metadata). `IID.Get<T>()` ([`IID.cs`](../../../src/Framework/Windows/Win32/IID.cs)) reads it via `T.Guid` on .NET and `default(T).Guid` on net472.\n\n## File Organization\n\n| Location | Contents |\n|---|---|\n| `src/Framework/Windows/Win32/System/Com/` | `ComScope`, `ComClassFactory`, `AgileComPointer`, `GlobalInterfaceTable` |\n| `src/Framework/Windows/Win32/IID.cs` | Generic IID lookup |\n| `src/Framework/Utilities/Wmi/`, `src/Framework/Shared/VisualStudio/`, `src/Tasks/AssemblyDependency/Fusion/`, `src/Tasks/AssemblyDependency/Metadata/`, `src/Tasks/TypeLib/` | Manual COM struct interfaces — own folder per API surface |\n\n## CS3016 CLS Compliance\n\nCsWin32 (since **0.3.287**) auto-emits `[CLSCompliant(false)]` on every generated COM struct wrapper that carries CCW thunks (the `[UnmanagedCallersOnly(CallConvs = new[] { ... })]` array argument is what trips CS3016), and adds CS3019 / CS3021 to its own generated-file `#pragma warning disable` list. So a CLS-compliant assembly (`[assembly: CLSCompliant(true)]`) builds clean with no consumer action — the hand-authored `GeneratedInteropClsCompliance.cs` partial and the `.editorconfig` CS3019 suppression for `{**/Windows/**/*.cs}` are gone.\n\nThe attribute is only emitted on .NET 5+ TFMs (net472 / netstandard2.0 have no CCW thunks, so no CS3016) and only for `internal` wrappers (the generator default). Don't re-add per-type `[CLSCompliant(false)]` partials or blanket `NoWarn`. See https://github.com/dotnet/roslyn/issues/68526 for the underlying Roslyn limitation that motivated the generator-side fix.\n",".github/skills/cswin32-interop/SKILL.md":"---\nname: cswin32-interop\ndescription: 'Guides CsWin32 P/Invoke interop in MSBuild. Consult when working with the PInvoke class, Windows.Win32 namespaces, FEATURE_WINDOWSINTEROP, HANDLE/HMODULE/HRESULT types, the generic BufferScope type, replacing [DllImport] with CsWin32, or conditioning Windows-only code for source builds.'\nargument-hint: 'Describe the Windows API or interop code you are migrating or adding.'\n---\n\n# CsWin32 Interop Guide\n\n[CsWin32](https://github.com/microsoft/CsWin32) replaces `[DllImport]` with source-generated `PInvoke.*` calls. `FEATURE_WINDOWSINTEROP` is the compile-time gate; source builds disable it.\n\n**Paired skill:** [cswin32-com](../cswin32-com/SKILL.md) covers struct-based COM interop on top of CsWin32 (`ComScope<T>`, `AgileComPointer<T>`, `delegate* unmanaged` vtables, `IComIID`, `CoCreateInstance`, manual COM structs not in Win32 metadata). This file covers only the general P/Invoke layer; the COM skill builds on its blittable-signature rules.\n\n## Rules\n\n1. **Replace `[DllImport]` with `PInvoke.*`**. Delete old declarations and hand-written structs/enums/constants.\n2. **Gate with `#if FEATURE_WINDOWSINTEROP`**, add runtime `IsWindows` check inside. Both required.\n3. **Use CsWin32 types directly** (`HANDLE`, `HMODULE`, `HRESULT.S_OK`, `FILE_FLAGS_AND_ATTRIBUTES`, etc.).\n4. **Call `PInvoke.*` directly** — no wrappers. Types flow via `InternalsVisibleTo`.\n5. **Prefer CsWin32 for Windows APIs**. Use `[LibraryImport]` only for non-Windows native calls (e.g. `libc`), guarded with `#if NET`.\n6. **Preserve the old error-handling contract.** Check the original `[DllImport]` for `PreserveSig` / `SetLastError` / `BOOL` / `HRESULT` semantics and reproduce them: `PreserveSig=false` → `.ThrowOnFailure()`; `SetLastError=true` + failed `BOOL` → `throw new Win32Exception()`. Silently returning where the old code threw is a behavior change. See [cswin32-com's parity table](../cswin32-com/SKILL.md#error-handling-parity-when-migrating) for the COM-side equivalent.\n\n## Blittable signatures\n\nCsWin32 is configured with `allowMarshaling: false`, so every `[DllImport]` and every manual\nCOM vtable method must be blittable — no marshalling at the boundary. These rules apply\nto both. For COM-vtable-only additions, see\n[cswin32-com](../cswin32-com/SKILL.md#blittable-vtable-signatures).\n\n- **Return `HRESULT`** from HRESULT-returning APIs (not `int`). Blittable (single `int` field),\n  exposes `.Succeeded` / `.Failed` / `.ThrowOnFailure()`. Use `HRESULT.S_OK` over `0`; cast\n  `e.HResult` to `(HRESULT)` when wrapping. `AddRef` / `Release` return `uint`.\n- **Call `.ThrowOnFailure()`** instead of `if (hr.Failed) Marshal.ThrowExceptionForHR(hr)` —\n  same exception, IErrorInfo-enriched, one-line call site:\n  `iface->Method(...).ThrowOnFailure();`. Branch on `hr` only when handling a specific HRESULT\n  (e.g. `ERROR_INSUFFICIENT_BUFFER`) before throwing. See\n  [cswin32-com's parity table](../cswin32-com/SKILL.md#error-handling-parity-when-migrating)\n  for the migration contract.\n- **Use `PCWSTR` / `PWSTR`** for wide strings, never managed `string`. Implicit conversion from\n  `fixed (char* p = managedString)`. Add to `NativeMethods.txt` if not yet generated.\n- **`T**` not `out T*`** for pointer outputs. `out` triggers marshaling + a `fixed` round-trip\n  at every call site.\n- **`void*` for opaque / reserved params** — never `IntPtr.Zero`; pass `null` literally.\n  `IntPtr` is fine at boundaries with the wider .NET surface (`Marshal.*`,\n  `SafeHandle.DangerousGetHandle`, public API).\n- **Prefer `nint` / `nuint`** over `IntPtr` / `UIntPtr` for native-sized integers — no boxing,\n  better cast semantics, no `IntPtr.Zero` ceremony.\n- **No managed reference types** (`string`, `StringBuilder`, arrays) in blittable signatures.\n- **Don't specify `PreserveSig = true` on `[DllImport]`** — it's the default. Use\n  `PreserveSig = false` only to force marshaller throw-on-failure (rare; prefer returning\n  `HRESULT` and `.ThrowOnFailure()`). `[ComImport]` defaults the opposite way, but struct-based\n  COM uses raw `delegate*` and isn't affected — see\n  [cswin32-com](../cswin32-com/SKILL.md#blittable-vtable-signatures).\n- **Constrain flag / option parameters to a typed `enum`.** When a native `DWORD` / `ULONG` /\n  `int` is documented as a `typedef enum` or `#define` set, declare a C# `[Flags] enum Foo : uint`\n  (matching the underlying type) and use it in the signature (and `delegate*` cast for COM).\n  Mirror the constraint even when the native side has no named enum. Self-documenting at the\n  call site: `OpenScope(path, CorOpenFlags.ofRead, ...)` vs `OpenScope(path, 0, ...)`. Co-locate\n  the enum next to its consumer. See\n  [`CorOpenFlags.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorOpenFlags.cs) and\n  [`CorAssemblyFlags.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorAssemblyFlags.cs).\n\n### Dual Guard Pattern\n\n```csharp\n#if FEATURE_WINDOWSINTEROP\n    if (IsWindows)\n    {\n        PInvoke.GetFileAttributesEx(fullPath, out WIN32_FILE_ATTRIBUTE_DATA data);\n    }\n#endif\n    // Cross-platform fallback\n```\n\n**WRONG**: `if (IsWindows) { #if FEATURE_WINDOWSINTEROP ... #endif }` — dead code in source builds.\n\nWindows-only files are excluded via `<Compile Remove>` instead — no `#if` inside needed.\n\n## Infrastructure\n\n**Define**: `src/Directory.BeforeCommon.targets` sets `FEATURE_WINDOWSINTEROP` + `$(FeatureWindowsInterop)` when `DotNetBuildSourceOnly != true`. Use `$(FeatureWindowsInterop)` in `.csproj` for `<Compile Remove>`/`<Compile Include>`.\n\n**CsWin32 config**: `src/Framework/NativeMethods.txt` (API list) + `NativeMethods.json` (`allowMarshaling: false`, `useSafeHandles: false`). Lives in Framework; other projects consume via `InternalsVisibleTo`. Do not add CsWin32 to other projects.\n\n**Guard selection**:\n\n| Guard | When | Runtime check? |\n|-------|------|----------------|\n| `#if FEATURE_WINDOWSINTEROP` | Multi-TFM Windows calls | Yes |\n| `#if FEATURE_WINDOWSINTEROP && NET` | Manual COM structs gated .NET-only (e.g. WMI). CsWin32-generated COM types via `ComScope<T>` work on net472 too — the generator emits `IComIID` on every TFM (see [cswin32-com](../cswin32-com/SKILL.md#icomiid-across-tfms)) | Yes |\n| `#if FEATURE_WINDOWSINTEROP && !NETSTANDARD` | CsWin32 types without `static abstract` (net472 + net10) | Yes |\n| `#if !NET` / `#if FEATURE_MSCOREE` | net472-only = inherently Windows | No |\n\n**Namespace imports** must be inside `#if FEATURE_WINDOWSINTEROP`. WDK APIs use `Windows.Wdk` namespace.\n\n**Files**: `src/Framework/Windows/` (CsWin32 partials), `src/Shared/Win32/` (COM helpers), `src/Framework/Utilities/Wmi/` (.NET-only COM structs).\n\n### Constant Replacements\n\n`NativeMethodsShared.S_OK` → `HRESULT.S_OK`, `InvalidHandle` → `HANDLE.INVALID_HANDLE_VALUE`, `FILE_ATTRIBUTE_DIRECTORY` → `FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY`, `STD_OUTPUT_HANDLE` → `STD_HANDLE.STD_OUTPUT_HANDLE`, `GENERIC_READ` → `FILE_ACCESS_RIGHTS.FILE_GENERIC_READ`. Pattern: `CsWin32EnumType.ORIGINAL_NAME` — check generated types in `obj/`.\n\n**Always prefer the generated enum over a local copy.** Before defining `private const int ERROR_*` / `private enum FooFlags`, grep the CsWin32 metadata:\n\n- `ERROR_*` (Win32 error codes) → `WIN32_ERROR.ERROR_*` (uint enum)\n- `HRESULT` codes → `HRESULT.S_OK`, etc.\n- Restart Manager status/type → `RM_APP_STATUS` / `RM_APP_TYPE`\n- File flags → `FILE_FLAGS_AND_ATTRIBUTES`, `FILE_ACCESS_RIGHTS`, `FILE_SHARE_MODE`, `FILE_CREATION_DISPOSITION`\n- Process flags → `PROCESS_CREATION_FLAGS`, `STARTUPINFOW_FLAGS`, `PROCESS_ACCESS_RIGHTS`\n- Memory mapping → `PAGE_PROTECTION_FLAGS`, `FILE_MAP`\n- Shell folder → `KNOWN_FOLDER_FLAG`\n\nCast `int`/`uint` return codes via `(WIN32_ERROR)res` for `switch` and equality. Add the enum to `NativeMethods.txt` if not yet generated, then check `obj/.../generated/Microsoft.Windows.CsWin32/.../Windows.Win32.<EnumName>.g.cs`.\n\n**Some flag values are standalone constants, not enum members.** A Win32 `#define` outside a `typedef enum` generates as an `internal const` on `PInvoke`. Example: the `LoadTypeLibEx` flags are `PInvoke.LOAD_TLB_AS_32BIT` / `_64BIT` (`uint`), **not** members of `REGKIND` (which has only `REGKIND_DEFAULT/REGISTER/NONE`). Add the *constant name* to `NativeMethods.txt` like any API. OR it onto an enum at the constant's width and cast back: `(REGKIND)((uint)REGKIND.REGKIND_NONE | PInvoke.LOAD_TLB_AS_32BIT)`. Don't reintroduce a local `const` CsWin32 already emits.\n\n**Match local types to the CsWin32 type.** Instead of `int res = (int)PInvoke.RmStartSession(...)` and casting at every comparison, declare `WIN32_ERROR res = PInvoke.RmStartSession(...)` and let helpers like `GetException(WIN32_ERROR res, ...)` take the typed value. Cast to `int`/`uint` only at the boundary where a non-CsWin32 API needs it (e.g. `new Win32Exception((int)res, ...)`). The same applies to `HRESULT`, `BOOL`, `HANDLE`, `PROCESS_CREATION_FLAGS`, etc.\n\n**Delete local mirror enums** that exist solely to mirror the Win32 one. The generated CsWin32 type is the source of truth.\n\n### FILETIME Conversions\n\nUse the helpers in [`src/Framework/Windows/Win32/Foundation/FileTimeExtensions.cs`](../../../src/Framework/Windows/Win32/Foundation/FileTimeExtensions.cs):\n\n- `fileTime.ToLong()` → 64-bit ticks\n- `fileTime.ToDateTime()` → local `DateTime` (FILETIME values returned as local time, e.g. `RM_PROCESS_INFO.ProcessStartTime`)\n- `fileTime.ToDateTimeUtc()` → UTC `DateTime` (FILETIME values returned as UTC, e.g. `WIN32_FILE_ATTRIBUTE_DATA.ftLastWriteTime`)\n\nDo **not** hand-roll `DateTime.FromFileTime((long)hi << 32 | lo)` — use the helpers for consistency. Note CsWin32-generated structs use `ComTypes.FILETIME` (int fields) for COM members and `Windows.Win32.Foundation.FILETIME` (uint fields) for kernel ones; the extension covers `ComTypes.FILETIME`.\n\n## BufferScope<T>\n\n`BufferScope<T>` (`src/Framework/Utilities/BufferScope.cs`) — stackalloc initial buffer with `ArrayPool<T>` fallback. Lives in Framework, available to all projects via `InternalsVisibleTo`.\n\n```csharp\nusing BufferScope<char> buffer = new(stackalloc char[(int)PInvoke.MAX_PATH]);\nint length = (int)PInvoke.GetShortPathName(path, buffer.AsSpan());\nif (length > buffer.Length)\n{\n    buffer.EnsureCapacity(length);\n    length = (int)PInvoke.GetShortPathName(path, buffer.AsSpan());\n}\nif (length > 0) path = buffer.Slice(0, length).ToString();\n```\n\n- `ref struct` — always use with `using`. Never stack-allocate more than 1024 bytes.\n- Check CsWin32 convenience overloads (e.g. `GetShortPathName(string, Span<char>)`) before writing `fixed` blocks.\n\n## Gotchas\n\n### CA1416 Platform Compatibility\n\nNo blanket `NoWarn` — handle semantically:\n- `if (IsWindows)` satisfies `[SupportedOSPlatform]` — no pragma needed\n- `if (IsUnixLike)` satisfies `[UnsupportedOSPlatform(\"windows\")]`\n- **Never use `!IsWindows`** — use `else if (IsUnixLike)`. See `documentation/specs/CA1416-analyzer-analysis.md`\n- Use versioned `[SupportedOSPlatform(\"windows6.1\")]` on methods calling CsWin32 APIs\n- `#pragma warning disable CA1416` only for **static local functions** (analyzer limitation)\n- CS0592 prevents `[SupportedOSPlatform]` on `partial struct` — put on individual members instead\n\n### Type Conversions\n\n- `HANDLE ↔ IntPtr`: `(HANDLE)intPtr` / `(IntPtr)h.Value`. Sentinels: `HANDLE.Null`, `HANDLE.INVALID_HANDLE_VALUE`\n- `FILETIME` conversion: `data.ftLastWriteTime.ToLong()`, `.ToDateTime()` (local), `.ToDateTimeUtc()` — see \"FILETIME Conversions\" above. CsWin32 uses `ComTypes.FILETIME` (int fields), not `Win32.Foundation.FILETIME`\n- `SafeFileHandle`: `new SafeFileHandle((IntPtr)h.Value, true)`, pass with `(HANDLE)handle.DangerousGetHandle()`\n- Nullable structs: `(SECURITY_ATTRIBUTES?)null`\n- Enum flags: use bitwise `&` — `HasFlag()` boxes on .NET Framework\n- Anonymous unions: `systemInfo.Anonymous.Anonymous.wProcessorArchitecture` — check generated source in `obj/`\n\n### Source-Build Verification (REQUIRED before pushing)\n\nSource builds (`DotNetBuildSourceOnly=true`) disable `FEATURE_WINDOWSINTEROP`. CI treats **all warnings as errors**. Run both builds before every push:\n\n```shell\n# Normal build\ndotnet msbuild MSBuild.Dev.slnf -v:q\n\n# Source-build — catches unused usings/members/docs from #if guards\ndotnet msbuild MSBuild.SourceBuild.slnf /p:DotNetBuildSourceOnly=true -v:q\n```\n\n**Everything** only referenced inside `#if FEATURE_WINDOWSINTEROP` must also be guarded:\n- **IDE0005**: `using` directives — most common failure\n- **IDE0051/IDE0052**: Private members (methods, fields, including helpers like `StringToByteArray`, constants like `ERROR_SHARING_VIOLATION`)\n- **CA1823**: Unused private fields (e.g. constants only consumed inside the guard)\n- **CS1587**: XML doc comments (move inside `#if`, not before)\n\nThe same applies when adding **polyfills** in `src/Framework/Polyfills/` (e.g. `SpanExtensions`, `IndexOfAnyExcept`): polyfills usually live behind `#if !NET` (or similar TFM guards) but are still consumed from `#if FEATURE_WINDOWSINTEROP` code paths. Always run the source-build to confirm the polyfill, its callers, and any helper members compile cleanly when interop is disabled — a polyfill referenced only by Windows-only code will trip IDE0051/CA1823 in source-only builds.",".github/skills/deploy-msbuild-to-vs/SKILL.md":"---\nname: deploy-msbuild-to-vs\ndescription: Deploy locally-built MSBuild binaries into a Visual Studio installation for testing and debugging. Use when you need VS itself to use your local MSBuild changes, or when debugging MSBuild as invoked by VS.\nargument-hint: Deploy local MSBuild to VS, patch VS with built MSBuild, debug MSBuild inside VS.\n---\n\n# Deploying Built MSBuild into Visual Studio\n\nThis skill guides you through replacing Visual Studio's bundled MSBuild with your locally-built binaries, so that builds initiated from within VS use your changes. It also covers debugging MSBuild when invoked by VS and restoring the original state.\n\n## When to Use\n\n- You need Visual Studio's build (Ctrl+Shift+B, Build Solution, design-time builds) to use your local MSBuild changes.\n- You are debugging an issue that only reproduces when MSBuild is called through the VS process (`devenv.exe`).\n- You want to test MSBuild changes against a real VS solution end-to-end.\n\n**If you only need command-line testing**, prefer the bootstrap approach instead (see the `use-bootstrap-msbuild` skill). It is faster and does not risk breaking your VS installation.\n\n## Prerequisites\n\n- Windows (the deploy script targets VS on Windows).\n- The latest Visual Studio 2026 installed with the components listed in [`.vsconfig`](../../../.vsconfig).\n- .NET Framework 3.5 installed on the Windows machine.\n- An **administrator** PowerShell prompt (required to write into `Program Files`).\n- The MSBuild repo cloned and buildable.\n\n## Step 1: Build MSBuild\n\nRun a full build from the repo root to produce the bootstrap output:\n\n```powershell\n.\\build.cmd\n```\n\nBy default this builds `Debug` configuration. If you want `Release`:\n\n```powershell\n.\\build.cmd /p:Configuration=Release\n```\n\nThe build creates bootstrap binaries under `artifacts\\bin\\MSBuild.Bootstrap\\{configuration}\\`.\n\n## Step 2: Locate Your VS MSBuild Bin Folder\n\nFind the MSBuild `Bin` folder inside your VS installation. The path depends on your VS version and edition.\n\n**⚠ Important**: If you have multiple VS versions or editions installed side-by-side, make sure you identify the correct installation path for the instance you want to patch. Confirm the path exists before deploying.\n\nThe general pattern is:\n\n```text\nC:\\Program Files\\Microsoft Visual Studio\\{version}\\{edition}\\MSBuild\\Current\\Bin\n```\n\nFor Visual Studio 2026, `{version}` is `18` (not `2026`). Common `{edition}` folders include `Enterprise`, `Professional`, and `Community`; this is customizable, so use `vswhere` below to confirm the path.\n\nExample paths:\n\n| VS Version | Edition | Typical Path |\n| ---------- | ------- | ------------ |\n| 2026 | Enterprise | `C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin` |\n| 2026 | Community | `C:\\Program Files\\Microsoft Visual Studio\\18\\Community\\MSBuild\\Current\\Bin` |\n\n### Finding the path automatically\n\nThe most reliable way to locate your VS installation is with `vswhere`:\n\n```powershell\n# List all VS installations with their paths\n& \"${env:ProgramFiles(x86)}\\Microsoft Visual Studio\\Installer\\vswhere.exe\" -all -prerelease -format table\n\n# Get the MSBuild Bin path for the latest installation\n$vsPath = & \"${env:ProgramFiles(x86)}\\Microsoft Visual Studio\\Installer\\vswhere.exe\" -latest -prerelease -property installationPath\n\"$vsPath\\MSBuild\\Current\\Bin\"\n```\n\nAlternatively, from a VS Developer Command Prompt:\n\n```cmd\nwhere msbuild\n```\n\nUse the directory containing that `MSBuild.exe`.\n\n## Step 3: Deploy with the Script\n\nFrom an **administrator** PowerShell, run:\n\n```powershell\n.\\scripts\\Deploy-MSBuild.ps1 -destination \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\"\n```\n\n### Script Parameters\n\n| Parameter | Default | Description |\n| --------- | ------- | ----------- |\n| `-destination` | *(required)* | The VS MSBuild `Bin` folder path |\n| `-configuration` | `Debug` | Must match the configuration used in `build.cmd` |\n| `-runtime` | `Detect` | Auto-detects `Desktop` vs `Core` from the path. Use `Desktop` for VS, `Core` for .NET SDK |\n| `-binDirectory` | `artifacts\\bin` | Override if build output is elsewhere |\n| `-makeBackup` | `$true` | Creates a timestamped `Backup-*` folder before overwriting |\n\n### What Gets Copied\n\nThe script copies from `artifacts\\bin\\MSBuild.Bootstrap\\{configuration}\\net472\\`:\n\n- **Core DLLs**: `Microsoft.Build.dll`, `Microsoft.Build.Framework.dll`, `Microsoft.Build.Tasks.Core.dll`, `Microsoft.Build.Utilities.Core.dll`, `Microsoft.NET.StringTools.dll`\n- **Targets files**: `Microsoft.Common.targets`, `Microsoft.CSharp.targets`, `Microsoft.VisualBasic.targets`, and many more\n- **Executables**: `MSBuild.exe` (x86 and amd64), `MSBuildTaskHost.exe`\n- **Framework dependencies**: Various `System.*` assemblies needed for .NET Framework\n\n## Step 4: Test in Visual Studio\n\n1. **Close and reopen** Visual Studio (it caches MSBuild on startup).\n2. Open a solution and build — VS now uses your locally-built MSBuild.\n3. Verify by checking the build output or binary log for your changes.\n\n## Restoring the Original MSBuild\n\nThe deploy script creates a backup folder at `{destination}\\Backup-{timestamp}\\` by default.\n\nTo restore:\n\n```powershell\n# Find the backup folder\nGet-ChildItem \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\\Backup-*\"\n\n# Copy everything back\nCopy-Item \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\\Backup-{timestamp}\\*\" `\n          \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\\\" -Recurse -Force\n```\n\nAlternatively, run **Visual Studio Installer → Modify → Repair** to fully restore the original binaries.\n\n⚠ **CAUTION**: If you overwrite MSBuild in Visual Studio and something goes wrong, VS itself may break (since VS uses MSBuild internally). Always keep the backup and know how to restore before deploying.\n\n## Debugging MSBuild Inside Visual Studio\n\nWhen MSBuild is invoked by VS (for real builds or design-time builds), you can attach a debugger using environment variables. Set these **before** launching VS:\n\n### Breakpoint at BuildManager.BeginBuild (Recommended for VS)\n\n```powershell\n$env:MSBuildDebugBuildManagerOnStart = \"1\"   # 1 = Debugger.Launch(), 2 = wait for attach\n$env:MSBuildDebugProcessName = \"devenv\"      # Only trigger in the VS process\ndevenv.exe\n```\n\nThis breaks into `BuildManager.BeginBuild`, which is where VS calls into MSBuild's API.\n\n### Breakpoint at MSBuild Entry Point\n\n```powershell\n$env:MSBUILDDEBUGONSTART = \"1\"   # 1 = Launch debugger, 2 = Wait for attach, 3 = Main process only\ndevenv.exe\n```\n\n### Capturing Full Binary Logs from VS\n\nVS heavily filters MSBuild events and suppresses them entirely during design-time builds. To capture everything:\n\n```powershell\n$env:MSBuildDebugEngine = \"1\"\n$env:MSBUILDDEBUGPATH = \"C:\\temp\\MSBuild_Logs\"   # Optional: override log output directory\n$env:MSBuildDebugProcessName = \"devenv\"            # Optional: only log from VS\ndevenv.exe\n```\n\nThis injects a binary log at `BuildManager.BeginBuild`, capturing full logs for both real and design-time builds. Logs are saved to `MSBuild_Logs\\` under the current directory (or `MSBUILDDEBUGPATH` if set).\n\n## Deploying to .NET SDK Instead\n\nTo patch a .NET SDK installation rather than VS:\n\n```powershell\n.\\scripts\\Deploy-MSBuild.ps1 -destination \"C:\\Program Files\\dotnet\\sdk\\10.0.100\" -runtime Core\n```\n\nThe script auto-detects SDK paths (containing `dotnet` and `sdk`), but you can force it with `-runtime Core`.\n\n## Cross-Machine Deployment\n\nIf you cannot build MSBuild on the target machine:\n\n1. Build and deploy to an **empty folder**:\n\n   ```powershell\n   .\\scripts\\Deploy-MSBuild.ps1 -destination \"C:\\temp\\msbuild-deploy\"\n   ```\n\n2. Copy the contents of that folder to the target machine's VS MSBuild Bin folder.\n3. Make a manual backup of the target machine's files first.\n\n## Troubleshooting\n\n| Problem | Solution |\n| ------- | -------- |\n| Access denied when deploying | Run PowerShell as **Administrator** |\n| VS won't start after deploy | Restore from `Backup-*` folder or run VS Installer Repair |\n| Changes not reflected in VS | Close all VS instances and reopen; kill lingering `MSBuild.exe` processes |\n| Configuration mismatch | Ensure `-configuration` matches what you passed to `build.cmd` |\n| Build errors in MSBuild repo | Run `.\\build.cmd` from a clean state; see [Something's wrong in my build](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Something's-wrong-in-my-build.md) |\n\n## Quick Reference\n\n| Step | Command |\n| ---- | ------- |\n| Build MSBuild | `.\\build.cmd` |\n| Deploy to VS (admin) | `.\\scripts\\Deploy-MSBuild.ps1 -destination \"{VS MSBuild Bin path}\"` |\n| Deploy Release build | `.\\scripts\\Deploy-MSBuild.ps1 -destination \"{path}\" -configuration Release` |\n| Debug MSBuild in VS | Set `$env:MSBuildDebugBuildManagerOnStart = \"1\"` then launch `devenv.exe` |\n| Capture VS binlogs | Set `$env:MSBuildDebugEngine = \"1\"` then launch `devenv.exe` |\n| Restore original | Copy back from `Backup-*` folder or run VS Installer Repair |\n\n## See Also\n\n- [Deploy-MSBuild documentation](https://github.com/dotnet/msbuild/blob/main/documentation/Deploy-MSBuild.md)\n- [Building and Debugging on Full Framework](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md)\n- [Bootstrap Documentation](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Bootstrap.md)\n",".github/skills/dotnet-aot-compat/SKILL.md":"---\nname: dotnet-aot-compat\ndescription: >\n  Make .NET projects compatible with Native AOT and trimming by systematically\n  resolving IL trim/AOT analyzer warnings. USE FOR: making projects AOT-compatible,\n  fixing trimming warnings, resolving IL warnings (IL2026, IL2070, IL2067, IL2072,\n  IL3050), adding DynamicallyAccessedMembers annotations, enabling IsAotCompatible.\n  DO NOT USE FOR: publishing native AOT binaries, optimizing binary size, replacing\n  reflection-heavy libraries with alternatives.\n  INVOKES: no tools — pure knowledge skill.\nlicense: MIT\n---\n\n# dotnet-aot-compat\n\nMake .NET projects compatible with Native AOT and trimming by systematically resolving all IL trim/AOT analyzer warnings.\n\n## When to Use This Skill\n\n- **\"Make this project AOT-compatible\"**\n- **\"Fix trimming warnings\"** or **\"fix IL warnings\"**\n- **\"Resolve IL2070 / IL2067 / IL2072 / IL2026 / IL3050 warnings\"**\n- **\"Add DynamicallyAccessedMembers annotations\"**\n- **\"Enable IsAotCompatible in my .csproj\"**\n- **\"My project has trim analyzer warnings after upgrading to net8.0\"**\n- **\"Annotate reflection code for the trimmer\"**\n\n## When Not to Use This Skill\n\nDo not use this skill when the project exclusively targets .NET Framework (net4x), which does not support the trim/AOT analyzers.\n\n## Prerequisites\n\nAn existing .NET project targeting net8.0 or later (or multi-targeting with at least one net8.0+ TFM) and the corresponding .NET SDK installed.\n\n## Background: What AOT Compatibility Means\n\nNative AOT and the IL trimmer perform static analysis to determine what code is reachable. Reflection can break this analysis because the trimmer can't see what types/members are accessed at runtime. The `IsAotCompatible` property enables analyzers that flag these issues as build warnings (ILXXXX codes).\n\n## Critical Rules\n\n### ❌ Never suppress warnings incorrectly\n\n- **NEVER** use `#pragma warning disable` for IL warnings. It hides warnings from the Roslyn analyzer at build time, but the IL linker and AOT compiler still see the issue. The code will fail at trim/publish time.\n- **NEVER** use `[UnconditionalSuppressMessage]`. It tells both the analyzer AND the linker to ignore the warning, meaning the trimmer cannot verify safety. Raising an error at build time is always preferable to hiding the issue and having it silently break at runtime.\n\n### 💡 Preferred approaches\n\n- **Prefer** `[DynamicallyAccessedMembers]` annotations to flow type information through the call chain.\n- **Prefer** refactoring to eliminate patterns that break annotation flow (e.g., boxing `Type` through `object[]`).\n- **Use** `[RequiresUnreferencedCode]` / `[RequiresDynamicCode]` / `[RequiresAssemblyFiles]` to mark methods as fundamentally incompatible with trimming, propagating the requirement to callers. This surfaces the issue clearly rather than hiding it — callers must explicitly acknowledge the incompatibility.\n\n### Annotation flow is key\n\nThe trimmer tracks `[DynamicallyAccessedMembers]` annotations through assignments, parameter passing, and return values. If this flow is broken (e.g., by boxing a `Type` into `object`, storing in an untyped collection, or casting through interfaces), the trimmer loses track and warns. The fix is to preserve the flow, not suppress the warning.\n\n## Step-by-Step Procedure\n\n> **Do not explore the codebase up-front.** The build warnings tell you exactly which files and lines need changes. Follow a tight loop: **build → pick a warning → open that file at that line → apply the fix recipe → rebuild**. Reading or analyzing source files beyond what a specific warning points you to is wasted effort and leads to timeouts. Let the compiler guide you.\n>\n> ❌ Do NOT run `find`, `ls`, or `grep` to understand the project structure before building. Do NOT read README, docs, or architecture files. Your first action should be Step 1 (enable AOT analysis), then build.\n\n### Step 1: Enable AOT analysis in the .csproj\n\nAdd `IsAotCompatible`. If the project doesn't exclusively target net8.0+, add a TFM condition (AOT analysis requires net8.0+):\n\n```xml\n<PropertyGroup>\n  <IsAotCompatible Condition=\"$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))\">true</IsAotCompatible>\n</PropertyGroup>\n```\n\nThis automatically sets `EnableTrimAnalyzer=true` and `EnableAotAnalyzer=true` for compatible TFMs. For multi-targeting projects (e.g., `netstandard2.0;net8.0`), the condition ensures no `NETSDK1210` warnings on older TFMs.\n\n### Step 2: Build and collect warnings\n\n```bash\ndotnet build <project.csproj> -f <net8.0-or-later-tfm> --no-incremental 2>&1 | grep 'IL[0-9]\\{4\\}'\n```\n\nSort and deduplicate. Common warning codes:\n- **IL2070**: Reflection call on a `Type` parameter missing `[DynamicallyAccessedMembers]`\n- **IL2067**: Passing an unannotated `Type` to a method expecting `[DynamicallyAccessedMembers]`\n- **IL2072**: Return value or extracted value missing annotation (often from unboxing)\n- **IL2057**: `Type.GetType(string)` with a non-constant argument\n- **IL2026**: Calling a method marked `[RequiresUnreferencedCode]`\n- **IL2050**: P/invoke method with COM marshalling parameters\n- **IL2075**: Return value flows into reflection without annotation\n- **IL2091**: Generic argument missing `[DynamicallyAccessedMembers]` required by constraint\n- **IL3000**: `Assembly.Location` returns empty string in single-file/AOT apps\n- **IL3050**: Calling a method marked `[RequiresDynamicCode]`\n\n### Step 3: Triage warnings by code (do NOT read every file)\n\nGroup the warnings from Step 2 by warning code and count them. **Do not open individual files yet.** Identify the top 1-2 patterns by count — these drive your fix strategy:\n\n| Pattern | Typical fix |\n|---------|-------------|\n| Many IL2026 + IL3050 from `JsonSerializer` | **Go to Strategy C immediately** — create a `JsonSerializerContext`, then batch-update all call sites |\n| IL2070/IL2087 on `Type` parameters | Add `[DynamicallyAccessedMembers]` to the innermost method, then cascade outward |\n| IL2067 passing unannotated `Type` | Annotate the parameter at the source |\n\n**In most real projects, IL2026/IL3050 from JsonSerializer dominate.** Start with Strategy C unless the warning breakdown clearly shows otherwise. After the batch JSON fix, handle remaining warnings with Strategies A–B. Only use Strategy D as a last resort.\n\n### Step 4: Fix warnings iteratively (innermost first)\n\nWork from the **innermost** reflection call outward. Each fix may cascade new warnings to callers.\n\n**Stay warning-driven.** For each warning, open only the file and line the compiler reported, identify the pattern, apply the matching fix recipe below, and move on. Do not scan the codebase for similar patterns or try to understand the full architecture — fix what the compiler tells you, rebuild, and let new warnings guide the next change. Fix a small batch of warnings (5-10), then rebuild immediately to check progress.\n\n**Use sub-agents when available.** If you can launch sub-agents (e.g., via a `task` tool), dispatch **multiple sub-agents in parallel** to edit different files simultaneously. Keep the main loop focused on building, parsing warnings, and dispatching — delegate actual file edits to sub-agents. For batch JSON updates, give each sub-agent 5-10 files to update in one prompt. **After 2 build-fix cycles, dispatch all remaining file edits to sub-agents in parallel — do not continue fixing files sequentially.** Example:\n\n> Update these files to use source-generated JSON: `src/Models/Resource.Serialization.cs`, `src/Models/Identity.Serialization.cs`, `src/Models/Plan.Serialization.cs`. In each file, replace `JsonSerializer.Serialize(writer, value)` with `JsonSerializer.Serialize(writer, value, MyProjectJsonContext.Default.TypeName)` and `JsonSerializer.Deserialize<T>(ref reader)` with `JsonSerializer.Deserialize(ref reader, MyProjectJsonContext.Default.TypeName)`. Only edit the JsonSerializer call sites.\n\n#### Strategy A: Add `[DynamicallyAccessedMembers]` (preferred)\n\nWhen a method uses reflection on a `Type` parameter, annotate the parameter to tell the trimmer what members are needed:\n\n```csharp\nusing System.Diagnostics.CodeAnalysis;\n\n// Before (warns IL2070):\nvoid Process(Type t) {\n    var method = t.GetMethod(\"Foo\");  // trimmer can't verify\n}\n\n// After (clean):\nvoid Process([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type t) {\n    var method = t.GetMethod(\"Foo\");  // trimmer preserves public methods\n}\n```\n\nWhen you annotate a parameter, **all callers** must now pass properly annotated types. This cascades outward — follow each caller and annotate or refactor as needed. **The caller's annotation must include at least the same member types as the callee's.** If the callee requires `PublicConstructors | NonPublicConstructors`, the caller must specify the same or a superset — using only `NonPublicConstructors` will produce IL2091.\n\n#### Strategy B: Refactor to preserve annotation flow\n\nWhen annotation flow is broken by boxing (storing `Type` in `object`, `object[]`, or untyped collections), **refactor** to pass the `Type` directly:\n\n```csharp\n// BROKEN: Type boxed into object[], annotation lost\nvoid Process(object[] args) {\n    Type t = (Type)args[0];  // IL2072: annotation lost through boxing\n    Evaluate(t, ...);\n}\n\n// FIXED: Pass Type as a separate, annotated parameter\nvoid Process(\n    object[] args,\n    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type calleeType,\n    ...) {\n    Evaluate(calleeType, ...);  // annotation flows cleanly\n}\n```\n\nCommon patterns that break flow and how to fix them:\n- **`object[]` parameter bags**: Extract the `Type` into a dedicated annotated parameter\n- **Dictionary/List storage**: Use a typed field with annotation instead\n- **Interface indirection**: Add annotation to the interface method's parameter\n- **Property with boxing getter**: Annotate the property's return type\n\n#### Strategy C: Source-generated JSON serialization (batch fix)\n\nWhen most warnings are IL2026/IL3050 from `JsonSerializer.Serialize`/`Deserialize`, this is a single mechanical fix applied in bulk:\n\n1. **Collect affected types** — grep for all `JsonSerializer.Serialize` and `JsonSerializer.Deserialize` call sites. Extract the type being serialized (the `<T>` in `Deserialize<T>`, or the runtime type of the object in `Serialize`).\n\n2. **Create one `JsonSerializerContext`** with `[JsonSerializable]` for every type found. **Skip types from external packages** (e.g., `ResponseError` from `Azure.Core`) — they won't source-generate for types you don't own. Handle external types separately via Gotcha #1 below.\n\n```csharp\n[JsonSerializerContext]\n[JsonSerializable(typeof(ManagedServiceIdentity))]\n[JsonSerializable(typeof(SystemData))]\n// ... one attribute per type YOU OWN\n// Do NOT add types from external packages (e.g., ResponseError)\ninternal partial class MyProjectJsonContext : JsonSerializerContext { }\n```\n\n3. **Batch-update all call sites** — do not read each file individually. Apply the pattern mechanically:\n   - `JsonSerializer.Serialize(obj)` → `JsonSerializer.Serialize(obj, MyProjectJsonContext.Default.TypeName)`\n   - `JsonSerializer.Deserialize<T>(json)` → `JsonSerializer.Deserialize(json, MyProjectJsonContext.Default.TypeName)`\n\n   Find and update all call sites in one pass:\n   ```bash\n   # Find all files with JsonSerializer calls\n   grep -rl 'JsonSerializer\\.\\(Serialize\\|Deserialize\\)' src/ --include='*.cs'\n   ```\n   Then use sequential `edit` calls to apply the same transformation to every matching file. **Do not use `sed` for C# code** — generics like `Deserialize<T>()` have angle brackets and nested parentheses that sed will mangle.\n\n4. **Build once** to verify. Remaining warnings will be non-serialization issues — handle those with Strategies A–B or D.\n\n#### Strategy D: `[RequiresUnreferencedCode]` (last resort)\n\nWhen a method fundamentally requires arbitrary reflection that cannot be statically described:\n\n```csharp\n[RequiresUnreferencedCode(\"Loads plugins by name using Assembly.Load\")]\npublic void LoadPlugin(string assemblyName) {\n    var asm = Assembly.Load(assemblyName);\n    // ...\n}\n```\n\nThis propagates to callers — they must also be annotated with `[RequiresUnreferencedCode]`. Use sparingly; it marks the entire call chain as trim-incompatible.\n\n### Step 5: Rebuild and repeat\n\nAfter each small batch of fixes (5-10 warnings), rebuild with `--no-incremental` and check for new warnings. **Do not attempt to fix all warnings before rebuilding** — frequent rebuilds catch mistakes early and reveal cascading warnings. Fixes cascade — annotating an inner method may surface warnings in its callers. Repeat until `0 Warning(s)`.\n\n### Step 6: Validate all TFMs\n\nBuild all target frameworks to ensure:\n- **0 IL warnings** on net8.0+ TFMs\n- **No NETSDK1210 warnings** (the `IsAotCompatible` condition handles this)\n- **Clean builds** on older TFMs (netstandard2.0, net472, etc.)\n\n```bash\ndotnet build <project.csproj>  # builds all TFMs\n```\n\n## Stop Signals\n\n- **Do not analyze more than 2-3 representative files per warning pattern.** After identifying the fix for a pattern, apply it to all matching files without reading each one first.\n- **Start fixing after one build.** Do not do a second analysis pass — begin implementing fixes for the most common warning pattern immediately after Step 3 triage.\n- Stop after achieving **0 IL warnings** for net8.0+ TFMs. Don't optimize or refactor already-clean annotations.\n- If a warning requires **architectural refactoring** beyond annotation flow fixes (e.g., replacing an entire serialization layer), document it and stop — don't rewrite large subsystems.\n- Limit to **3 build-fix iterations** per warning. If annotation flow doesn't resolve it after 3 attempts, escalate to `[RequiresUnreferencedCode]`.\n- Don't chase warnings in **third-party dependencies** you can't modify. Note them and move on.\n- If the user asked a scoped question (e.g., \"fix warnings in this file\"), don't expand to the entire project.\n\n## Polyfills for Older TFMs\n\nFor multi-targeting projects that include netstandard2.0 or net472, you need polyfills for `DynamicallyAccessedMembersAttribute` and related types. See [references/polyfills.md](references/polyfills.md).\n\n## Common Gotchas\n\n1. **External types without AOT-safe serialization**: When a type comes from a dependency you can't modify (e.g., `ResponseError` from `Azure.Core`) and it lacks a source-generated serializer, `Options.GetConverter<T>()` is reflection-based and will produce IL warnings. First check if the type implements `IJsonModel<T>` (common in Azure SDK) — if so, bypass `JsonSerializer` entirely:\n\n```csharp\n// Before (IL2026 — JsonSerializer uses reflection):\nJsonSerializer.Serialize(writer, errorValue);\n\n// After (AOT-safe — uses IJsonModel directly):\n((IJsonModel<ResponseError>)errorValue).Write(writer, ModelReaderWriterOptions.Json);\n\n// For deserialization:\nvar error = ((IJsonModel<ResponseError>)new ResponseError()).Create(ref reader, ModelReaderWriterOptions.Json);\n```\n\nDo **not** add the external type to your `JsonSerializerContext` — it won't source-generate for types you don't own. If the type doesn't implement `IJsonModel<T>`, write a custom `JsonConverter<T>` with manual `Utf8JsonReader`/`Utf8JsonWriter` logic and register it via `[JsonSourceGenerationOptions]` on your context.\n\n2. **Serialization libraries**: Most reflection-based serializers (e.g., `Newtonsoft.Json`, `XmlSerializer`) are not AOT-compatible. Migrate to a source-generation-based serializer such as `System.Text.Json` with a `JsonSerializerContext`. If migration is not feasible, mark the serialization call site with `[RequiresUnreferencedCode]`.\n\n3. **Shared projects / projitems**: When source is shared between multiple projects via `<Import>`, annotations added to shared code affect ALL consuming projects. Verify that all consumers still build cleanly.\n\n## References\n\n[Limitations](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=windows%2Cnet8#limitations-of-native-aot-deployment)\n[Conceptual: Understanding trimming](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-concepts)\n[How-to: trim compat](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/fixing-warnings)\n\n## Checklist\n\n- [ ] Added `<IsAotCompatible>` with TFM condition to .csproj\n- [ ] Built with AOT analyzers enabled (net8.0+ TFM)\n- [ ] Fixed all IL warnings via annotations or refactoring\n- [ ] No `#pragma warning disable` or `[UnconditionalSuppressMessage]` used for any IL warning\n- [ ] Polyfills present for older TFMs if needed\n- [ ] All target frameworks build with 0 warnings\n- [ ] Verified shared/linked source doesn't break sibling projects\n",".github/skills/dotnet-aot-compat/references/polyfills.md":"# Polyfills for Older TFMs\n\n`DynamicallyAccessedMembersAttribute` shipped in .NET 5. For projects targeting netstandard2.0 or net472, you need a polyfill. The trimmer recognizes the attribute by name, so a local copy works:\n\n```csharp\n#if !NET\nnamespace System.Diagnostics.CodeAnalysis\n{\n    [AttributeUsage(AttributeTargets.Field | AttributeTargets.ReturnValue |\n                    AttributeTargets.GenericParameter | AttributeTargets.Parameter |\n                    AttributeTargets.Property, Inherited = false)]\n    internal sealed class DynamicallyAccessedMembersAttribute : Attribute\n    {\n        public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes)\n            => MemberTypes = memberTypes;\n        public DynamicallyAccessedMemberTypes MemberTypes { get; }\n    }\n\n    [Flags]\n    internal enum DynamicallyAccessedMemberTypes\n    {\n        None = 0,\n        PublicParameterlessConstructor = 0x0001,\n        PublicConstructors = 0x0002 | PublicParameterlessConstructor,\n        NonPublicConstructors = 0x0004,\n        PublicMethods = 0x0008,\n        NonPublicMethods = 0x0010,\n        PublicFields = 0x0020,\n        NonPublicFields = 0x0040,\n        PublicNestedTypes = 0x0080,\n        NonPublicNestedTypes = 0x0100,\n        PublicProperties = 0x0200,\n        NonPublicProperties = 0x0400,\n        PublicEvents = 0x0800,\n        NonPublicEvents = 0x1000,\n        Interfaces = 0x2000,\n        All = ~None // Discouraged — prefer specific flags\n    }\n}\n#endif\n```\n\nSimilarly for `RequiresUnreferencedCodeAttribute` and `UnconditionalSuppressMessageAttribute` if needed on older TFMs.\n",".github/skills/integrating-sdk-and-msbuild/SKILL.md":"---\nname: integrating-sdk-and-msbuild\ndescription: 'Guides work on the SDK-MSBuild integration boundary. Consult when authoring or modifying SDK targets, working on dotnet CLI to MSBuild invocation, handling project-reference protocol, coordinating cross-repo changes with dotnet/sdk, debugging property resolution or import ordering, working on restore/build/publish/pack target chains, or dealing with Directory.Build.props/targets interaction.'\nargument-hint: 'Describe the SDK integration scenario or cross-repo coordination need.'\n---\n\n# SDK-MSBuild Integration Patterns\n\nMSBuild operates as a component within the .NET SDK. This boundary is the most complex integration point in the .NET build stack, spanning MSBuild (engine), SDK (target implementations), NuGet (restore), and Roslyn (compilation).\n\n## The Evaluation Boundary\n\nUnderstanding MSBuild's evaluation order is critical for SDK target authoring:\n\n```\n1. Environment variables\n2. Global properties (from CLI: -p:Foo=Bar)\n3. Project-level properties (file order, with imports):\n   ┌─ Sdk.props (SDK defaults)\n   ├─ Directory.Build.props (user overrides BEFORE project)\n   ├─ <Project> properties (the .csproj itself)\n   ├─ Directory.Build.targets (user overrides AFTER project)\n   └─ Sdk.targets (SDK target definitions)\n4. Item definitions\n5. Items (including SDK default globs)\n```\n\n### Key Import Order Rules\n\n- **SDK props import BEFORE user project** — SDK defaults can be overridden by the user\n- **SDK targets import AFTER user project** — SDK targets see user-specified properties\n- **`Directory.Build.props` is imported from `Microsoft.Common.props` as an early user extension point after core defaults are computed** — use it for solution-wide customization\n- **Property defaults set in SDK must not override user-specified values** — always use `Condition=\"'$(Prop)' == ''\"`\n\n```xml\n<!-- CORRECT: SDK default that respects user override -->\n<OutputType Condition=\"'$(OutputType)' == ''\">Library</OutputType>\n\n<!-- WRONG: Unconditional set clobbers user's .csproj -->\n<OutputType>Library</OutputType>\n```\n\n## Restore and Build Separation\n\n**Restore and Build must never run in the same evaluation.** The restore phase generates `.g.props` and `.g.targets` files that must be imported during evaluation — but they don't exist until restore completes.\n\n- `dotnet build` implicitly runs restore then build as **separate invocations**\n- `dotnet build --no-restore` skips restore, assuming it already happened\n- Running both targets in one invocation (`/t:Restore;Build`) is a known anti-pattern that causes intermittent failures\n\n## Project Reference Protocol\n\nThe project-reference protocol spans MSBuild, SDK, and NuGet. It is the most complex integration boundary.\n\n### How It Works\n\n1. Outer build dispatches to `_GetProjectReferenceTargetFrameworkProperties` to determine inner build parameters\n2. Inner build runs with the resolved `TargetFramework` (singular) for each referenced project\n3. `GetTargetPath` returns the output assembly for the referencing project to consume\n\n### Rules\n\n- Protocol changes must be coordinated across MSBuild, SDK, and NuGet teams\n- Multi-targeting projects (`<TargetFrameworks>`) dispatch multiple inner builds\n- The outer build must not assume a single target framework\n- `SetTargetFramework` is how the outer build communicates the chosen framework to inner builds\n\n## Target Authoring in SDK Context\n\n### Extension Points\n\nSDK provides well-known extension points for targets:\n\n| Extension Point | Use For |\n|----------------|---------|\n| `$(BuildDependsOn)` | Adding to the Build chain |\n| `$(CompileDependsOn)` | Pre-compilation steps |\n| `$(PublishDependsOn)` | Publish pipeline additions |\n| `$(PackDependsOn)` | NuGet pack pipeline additions |\n| `BeforeTargets=\"Build\"` | Use sparingly; prefer `DependsOnTargets` |\n\n### Ordering Rules\n\n1. **Use `DependsOnTargets` for required predecessors** — it's explicit and predictable\n2. **`BeforeTargets`/`AfterTargets` should be used sparingly** — they create implicit ordering that's hard to debug\n3. **Incremental build targets need precise `Inputs` and `Outputs`** — incorrect declarations cause either rebuild-every-time or stale-output bugs\n4. **Test with multi-targeting** — target chains execute once per `TargetFramework` in the inner build\n\n## Cross-Repo Coordination\n\nChanges that touch the MSBuild-SDK boundary often require coordinated PRs:\n\n1. **MSBuild engine change** → may need SDK target updates\n2. **SDK target change** → may need MSBuild API additions\n3. **NuGet restore change** → affects both MSBuild evaluation and SDK targets\n\n### Coordination Protocol\n\n- File an issue in both repos describing the cross-cutting change\n- Land the MSBuild change first (lower in the stack)\n- Update SDK to consume the new MSBuild via dependency flow\n- Test end-to-end with the SDK's MSBuild integration tests\n\n## Design-Time Builds\n\nVisual Studio uses design-time builds with different target contracts:\n\n- Design-time builds call `ResolveProjectReferences` but not `Build`\n- They set `$(DesignTimeBuild)=true` and `$(BuildingProject)=false`\n- Targets that should not run during design-time must check these properties\n- Design-time builds must be fast — avoid expensive I/O or compilation\n\n## Common Integration Bugs\n\n| Symptom | Likely Cause |\n|---------|-------------|\n| Property has wrong value | Import ordering — check if SDK prop overrides user setting |\n| Target runs in wrong order | Missing `DependsOnTargets` declaration |\n| Build works, restore fails | Evaluation-time dependency on restore-generated files |\n| Works single-target, fails multi-target | Target assumes single `$(TargetFramework)` |\n| CLI build works, VS build fails | Design-time build target contract violation |\n",".github/skills/maintaining-binary-log-compatibility/SKILL.md":"---\nname: maintaining-binary-log-compatibility\ndescription: 'Guides changes to MSBuild binary log infrastructure. Consult when modifying BinaryLogger or BinaryLogReplayEventSource, adding new BuildEventArgs types, changing event serialization/deserialization, modifying ProjectImportsCollector, adjusting message importance levels, or making changes that affect .binlog content. Also applies when verifying that behavioral changes are properly reflected in binary log output.'\nargument-hint: 'Describe the binary log change or event serialization concern.'\n---\n\n# Binary Log Considerations\n\nThe binary log (`.binlog`) is MSBuild's primary diagnostic format and source of truth for build analysis. It captures the complete build event stream with full fidelity. Format changes have strict compatibility requirements.\n\nFor general binary log usage, see [Binary-Log.md](../../../documentation/wiki/Binary-Log.md).\n\n## Core Principles\n\n1. **The binlog captures everything.** All meaningful build events must be represented. If your change alters build behavior, it must be observable in the binlog.\n2. **Format changes must be backward-compatible.** Older versions of MSBuild Structured Log Viewer and other tools must be able to read binlogs produced by newer MSBuild, at minimum gracefully degrading.\n3. **Forward compatibility matters too.** Newer viewers should handle binlogs from older MSBuild without crashing, even if some events are unrecognized.\n\n## Architecture Overview\n\n```\nBuild Engine\n  → BuildEventArgs (structured events)\n    → BinaryLogger (serializes to .binlog)\n      → ProjectImportsCollector (embeds project/targets files)\n\nReplay:\n  .binlog file\n    → BinaryLogReplayEventSource (deserializes)\n      → Any ILogger (console, structured log viewer, analyzers)\n```\n\nKey source files:\n- `src/Build/Logging/BinaryLogger/BinaryLogger.cs` — the logger itself\n- `src/Framework/BuildEventArgs.cs` — base class for all build events\n- `src/Build/Logging/BinaryLogger/ProjectImportsCollector.cs` — captures imported files\n\n## Adding New Build Event Types\n\nWhen adding a new `BuildEventArgs` subclass:\n\n1. **Define the new event class** inheriting from the appropriate base (`BuildMessageEventArgs`, `BuildWarningEventArgs`, etc.)\n2. **Add serialization and forwarding support** — implement `WriteToStream` and `CreateFromStream`, then trace the event through every dispatcher and transport (including task-host forwarding) without making unknown event types fatal\n3. **Increment the binary log version** if the new event type changes the format\n4. **Add a new record type constant** in the binary logger's record type enum\n5. **Handle the unknown-type case** in the replay source — older readers must skip gracefully\n\n### Serialization Compatibility Rules\n\n- **Never remove fields** from existing event args serialization\n- **New fields must be appended** to the end of the serialization stream\n- **Use version checks** when reading — if the binlog version is older, use defaults for new fields\n- **Nullable fields** should serialize a presence flag before the value\n\n```csharp\n// Pattern for backward-compatible field addition\nif (logVersion >= newFieldVersion)\n{\n    writer.Write(newField);\n}\n\n// Reading with backward compatibility\nif (logVersion >= newFieldVersion)\n{\n    newField = reader.ReadString();\n}\nelse\n{\n    newField = defaultValue;\n}\n```\n\n## Message Importance Levels\n\nImportance controls what appears in console output, but **everything goes to binlog** regardless of importance.\n\n| Level | Use For | Console Verbosity |\n|-------|---------|------------------|\n| `High` | Critical user-facing information | Minimal and above |\n| `Normal` | Standard build progress | Normal and above |\n| `Low` | Detailed diagnostic information | Detailed and above |\n| `Diagnostic` | Internal debugging | Diagnostic only |\n\n### Rules\n\n- Default to `Normal` for user-relevant information\n- Use `Low` for information useful when debugging but noisy in normal builds\n- `High` is reserved for important warnings/status — use sparingly\n- Never skip logging because \"it's too verbose\" — log at `Low` instead\n\n## ProjectImportsCollector\n\nThe `ProjectImportsCollector` embeds all imported `.props`, `.targets`, and project files into the binlog. This enables the \"preprocessed view\" in log viewers.\n\n### Considerations\n\n- `MSBUILDLOGIMPORTS=1` (or the `/bl` switch) enables import collection\n- Imported file content is captured at evaluation time — reflects the actual content used\n- Large import chains increase binlog size — this is acceptable for diagnostic completeness\n- Sensitive content in imported files will be embedded — document this for users\n\n## Changes That Affect Binlog Content\n\nWhen modifying MSBuild behavior, verify binlog impact:\n\n| Change Type | Binlog Consideration |\n|------------|---------------------|\n| New property set during evaluation | Appears in `PropertyInitialValue` or `PropertyReassignment` events |\n| New target added | Produces `TargetStarted`/`TargetFinished` events |\n| Changed task behavior | Task output items/properties captured in `TaskFinished` |\n| New warning/error | Captured as `BuildWarningEventArgs`/`BuildErrorEventArgs` |\n| Modified import chain | Changes which files `ProjectImportsCollector` captures |\n\n## Testing Binlog Changes\n\n- **Round-trip test**: Write a binlog, replay it, verify all events are reconstructed\n- **Version compatibility test**: Verify older replay sources handle new events gracefully\n- **Content verification**: Assert specific events appear in the binlog for behavioral changes\n- **Size regression**: Monitor binlog size for unexpectedly large increases\n- Use `BinaryLogReplayEventSource` in tests to verify binlog content programmatically\n\n## Checklist\n\n- [ ] New event types have `WriteToStream`/`CreateFromStream` implementations\n- [ ] Binary log format version incremented if format changed\n- [ ] Backward compatibility: older readers skip/degrade gracefully\n- [ ] Forward compatibility: newer readers handle old format\n- [ ] Importance levels set correctly for new messages\n- [ ] Behavioral changes produce observable binlog events\n- [ ] Round-trip test passes (serialize → deserialize → verify)\n",".github/skills/merge-dependency-updates/SKILL.md":"---\nname: merge-dependency-updates\ndescription: 'Review and merge open bot PRs: dependency updates from dotnet-maestro, codeflow from dotnet/dotnet, and OneLoc localization PRs. Produces a clickable dashboard with CI status, review state, and suspicious file flags. Use when you want to triage all open bot PRs in one pass.'\nargument-hint: 'Triage, update, and version-bump open bot PRs.'\n---\n\n# Managing Bot PRs (Dependencies, Codeflow, OneLoc)\n\nThis skill triages all open bot PRs in the MSBuild repo, produces a review dashboard, then optionally updates branches and bumps versions for merge readiness.\n\n## When to Use\n\n- You want to see the status of all open bot PRs at a glance\n- There are open dependency update / codeflow / OneLoc PRs that need merging\n- You want to process all of them in one pass with a clickable summary\n\n## PR Categories\n\n| Category | Author | Title Pattern | Example |\n|----------|--------|---------------|---------|\n| Dependency update | `dotnet-maestro[bot]` | `[<branch>] Update dependencies from dotnet/<repo>` | `[vs18.3] Update dependencies from dotnet/arcade` |\n| Codeflow | `dotnet-maestro[bot]` | `[<branch>] Source code updates from dotnet/dotnet` | `[vs18.3] Source code updates from dotnet/dotnet` |\n| OneLoc | `dotnet-bot` | `Localized file check-in by OneLocBuild Task: ...` | `Localized file check-in by OneLocBuild Task: Build definition ID 9434: Build ID ...` |\n\n## Step-by-Step Procedure\n\n### Step 1: Gather All Bot PRs\n\nRun **two searches in parallel** to find all open bot PRs:\n\n1. **Dependency updates + Codeflow:**\n   ```\n   Search: is:open author:app/dotnet-maestro repo:dotnet/msbuild\n   ```\n\n2. **OneLoc localization:**\n   ```\n   Search: is:open author:dotnet-bot repo:dotnet/msbuild (\"OneLocBuild\" OR \"Localized file check-in\")\n   ```\n\n### Step 2: Analyze Each PR (in parallel)\n\nFor each PR, query the following data using `gh api graphql`. Process PRs in parallel batches for speed.\n\n```bash\ngh api graphql -f query='\n  query {\n    repository(owner: \"dotnet\", name: \"msbuild\") {\n      pullRequest(number: <PR_NUMBER>) {\n        id\n        title\n        baseRefName\n        headRefName\n        mergeable\n        reviewDecision\n        commits(last: 1) {\n          nodes {\n            commit {\n              statusCheckRollup {\n                state\n              }\n            }\n          }\n        }\n        reviews(last: 10) {\n          nodes {\n            state\n            author { login }\n          }\n        }\n        reviewThreads(first: 20) {\n          nodes {\n            isResolved\n            comments(first: 1) {\n              nodes { body author { login } }\n            }\n          }\n        }\n        files(first: 50) {\n          nodes { path additions deletions }\n          totalCount\n        }\n      }\n    }\n  }\n'\n```\n\nFrom the response, extract:\n- **CI status**: from `statusCheckRollup.state` — `SUCCESS`, `FAILURE`, `PENDING`, `ERROR`, `EXPECTED`, or `null`. Map `ERROR` to failure (❌) and `EXPECTED` to neutral (⚪) in the dashboard.\n- **Review decision**: `APPROVED`, `REVIEW_REQUIRED`, `CHANGES_REQUESTED`\n- **Approvals**: count and list of `APPROVED` reviews\n- **Review comments**: any unresolved threads from the first 20 results (especially `CHANGES_REQUESTED`)\n- **Files changed**: use `totalCount` for the file count; use the first 50 file paths from `files.nodes` for suspicious file detection\n- **Suspicious flag**: check if any file paths are outside expected patterns (see below)\n\n#### Suspicious File Detection\n\n| PR Category | Expected Files | Flag if... |\n|-------------|---------------|------------|\n| Dependency update (arcade) | `eng/**`, `global.json` | Any file outside `eng/` or `global.json` |\n| Dependency update (roslyn/nuget/runtime) | `eng/Version.Details.xml`, `eng/Version.Details.props` | Any file outside `eng/` |\n| Codeflow (dotnet/dotnet) | `eng/Version.Details.xml` | Any `src/` files modified |\n| OneLoc | `**/xlf/*.xlf`, `**/Resources/*.resx` | Any non-localization file, or reviewers flagging reverted translations |\n\n### Step 3: Update Branches & Bump Versions (dependency PRs only)\n\nFor each **dependency update** PR:\n\n#### 3a. Update the PR branch\n\nUse the GitHub GraphQL `updatePullRequestBranch` mutation — the exact equivalent of the \"Update branch\" button:\n\n```bash\ngh api graphql -f query='\n  mutation {\n    updatePullRequestBranch(input: {pullRequestId: \"<NODE_ID>\"}) {\n      pullRequest { number headRefOid }\n    }\n  }\n'\n```\n\n#### 3b. Bump VersionPrefix (vs < 18.10 branches only)\n\n**Skip for `vs18.10` and newer vs* branches** - they don't need to increment the patch version with every dependency update anymore. Only bump for `vs*` branches that are older than `vs18.10`.\n\n1. Read `<VersionPrefix>X.Y.Z</VersionPrefix>` from the **target branch's** `eng/Versions.props`\n2. Increment patch: `X.Y.Z` → `X.Y.(Z+1)`\n3. Checkout the PR branch, update the version, commit, push:\n   ```bash\n   git fetch origin <head-branch>\n   git checkout <head-branch>\n   # Edit eng/Versions.props: set <VersionPrefix> to X.Y.(Z+1)\n   git add eng/Versions.props\n   git commit -m \"Bump VersionPrefix to X.Y.(Z+1)\"\n   git push origin <head-branch>\n   ```\n\n**Note:** The `<VersionPrefix>` line may be inline with `<DotNetFinalVersionKind>` — preserve that formatting.\n\n### Step 4: Produce the Dashboard\n\nOutput a **single dashboard** with all PRs grouped by category. This is the most important output — it lets the user click through and approve/reject each PR.\n\nFormat:\n\n```\n## 🚦 Dependency Update PRs\n\n| PR | Branch | CI | Review | Files | Suspicious? | Link |\n|----|--------|-----|--------|-------|-------------|------|\n| #13281 | main ← roslyn | ✅ SUCCESS | ✅ APPROVED (1) | 2 | ✅ Clean | https://github.com/dotnet/msbuild/pull/13281 |\n| #13310 | vs17.10 ← arcade | ❌ FAILURE | 🔶 REVIEW_REQUIRED | 14 | ✅ Clean (eng/ only) | https://github.com/dotnet/msbuild/pull/13310 |\n\n## 🔄 Codeflow PRs\n\n| PR | Branch | CI | Review | Files | Suspicious? | Link |\n|----|--------|-----|--------|-------|-------------|------|\n| #13258 | vs18.3 ← dotnet/dotnet | ✅ SUCCESS | 🔶 REVIEW_REQUIRED | 1 | ✅ Clean | https://github.com/dotnet/msbuild/pull/13258 |\n\n## 🌐 OneLoc PRs\n\n| PR | Branch | CI | Review | Files | Suspicious? | Link |\n|----|--------|-----|--------|-------|-------------|------|\n| #13290 | main | ⚪ None | ⛔ CHANGES_REQUESTED | 65 | ⚠️ Reverts translations | https://github.com/dotnet/msbuild/pull/13290 |\n```\n\nStatus icons:\n- CI: ✅ SUCCESS, ❌ FAILURE/ERROR, ⏳ PENDING, ⚪ None/EXPECTED\n- Review: ✅ APPROVED, 🔶 REVIEW_REQUIRED, ⛔ CHANGES_REQUESTED\n- Suspicious: ✅ Clean, ⚠️ with description\n\nAfter the tables, add a summary:\n> **N PRs total. M ready to merge, K need reviews, J have CI failures, L blocked.**\n\n## Version File Reference\n\nThe version is defined in `eng/Versions.props`:\n\n```xml\n<PropertyGroup>\n  <VersionPrefix>18.3.3</VersionPrefix>\n</PropertyGroup>\n```\n\n| Branch Pattern | VersionPrefix Format | Bump Needed? | Pre-release Label |\n|---------------|---------------------|-------------|------------------|\n| `main`        | `Major.Minor.0`     | No          | `preview`        |\n| `vs*`         | `Major.Minor.Patch` | Yes (+1)    | `servicing` or `preview` |\n\n## Troubleshooting\n\n| Problem | Solution |\n|---------|----------|\n| `updatePullRequestBranch` fails | PR may have conflicts GitHub can't auto-resolve — fall back to local `git merge` |\n| Push rejected after version bump | Branch was updated by maestro; re-fetch and retry |\n| VersionPrefix has no patch component (e.g. `18.6.0` on main) | This is main — skip the bump |\n| OneLoc PR flagged as \"reverting translations\" | Do NOT merge — wait for updated translations |\n",".github/skills/optimizing-msbuild-performance/SKILL.md":"---\nname: optimizing-msbuild-performance\ndescription: 'Guides performance optimization for MSBuild engine code. Consult when working on hot paths in evaluation or execution, reducing allocations, choosing collection types, handling strings efficiently, modifying Expander.cs or Evaluator.cs, using Span-based APIs and stackalloc, caching values, or profiling build performance. Also applies when reviewing PRs for performance regression.'\nargument-hint: 'Describe the performance-sensitive code area or optimization goal.'\n---\n\n# MSBuild Performance Guidelines\n\nMSBuild evaluates and builds thousands of projects in enterprise solutions. Performance is an architectural concern, not an afterthought.\n\n## Guiding Principle\n\n**Profile before optimizing; measure, do not guess.** Use BenchmarkDotNet, the evaluation profiler (`/profileevaluation`), or ETW traces to identify actual bottlenecks before optimizing.\n\nSee [evaluation-profiling.md](../../../documentation/evaluation-profiling.md) and [General_perf_onepager.md](../../../documentation/specs/proposed/General_perf_onepager.md) for profiling tools.\n\n## Allocation Awareness on Hot Paths\n\nThe evaluation and execution engines process millions of operations per build. Unnecessary allocations cause GC pressure that compounds across large solutions.\n\n### Rules\n\n1. **Avoid LINQ in hot paths.** `Where`, `Select`, `Any`, `First` all allocate enumerator objects and delegate closures. Use `foreach` loops instead.\n\n   ```csharp\n   // BAD — allocates iterator + delegate\n   var match = items.FirstOrDefault(i => i.Name == name);\n\n   // GOOD — zero allocations\n   foreach (var item in items)\n   {\n       if (item.Name == name) { /* found */ break; }\n   }\n   ```\n\n2. **Avoid string allocations in formatting.** Use `string.Concat`, interpolation with `Span<T>`, or `StringBuilder` reuse — not `string.Format` on hot paths.\n\n3. **Prefer `Span<T>` and `stackalloc` for short-lived buffers** when parsing or slicing strings. Avoid `Substring` when you only need to compare or inspect a portion.\n\n4. **Cache computed values.** If a value is computed in a loop but doesn't change, hoist it out. If it's expensive and reused across calls, use a field or `Lazy<T>`.\n\n## String Comparison Rules\n\nMSBuild property, item, and target names are **case-insensitive**. Getting this wrong causes subtle bugs and perf issues.\n\n| Scenario | Use |\n|----------|-----|\n| Property/item/target names | `MSBuildNameIgnoreCaseComparer` |\n| General MSBuild identifiers | `StringComparison.OrdinalIgnoreCase` |\n| Dictionary keys for MSBuild names | `MSBuildNameIgnoreCaseComparer` as comparer |\n| File paths on Windows | `StringComparison.OrdinalIgnoreCase` |\n| File paths on Linux | `StringComparison.Ordinal` |\n\n**Never** use `ToLower()`/`ToUpper()` for comparisons — they allocate a new string every time.\n**Never** use `CurrentCulture` for MSBuild identifiers — build behavior must not vary by locale.\n\n## Collection Type Selection\n\n| Access Pattern | Recommended Type |\n|---------------|-----------------|\n| Small fixed set (< ~8 items) | Array or `ReadOnlySpan<T>` |\n| Build-once, read-many | `ImmutableArray<T>` (not `ImmutableList<T>`) |\n| Keyed lookup, many items | `Dictionary<TKey, TValue>` with appropriate comparer |\n| Keyed lookup, few items (< 5) | Linear scan over array (cache-friendly, avoids dict overhead) |\n| Concurrent reads, rare writes | `ImmutableDictionary` or snapshot pattern |\n| Ordered iteration needed | `List<T>` or array; avoid `HashSet<T>` if order matters |\n\n**`ImmutableList<T>` is almost never the right choice** — it has O(log n) access vs O(1) for `ImmutableArray<T>`.\n\n## Hot Path Identification\n\nThese areas are performance-critical and require extra scrutiny:\n\n- **`Expander.cs`** — property/item/metadata expansion during evaluation\n- **`Evaluator.cs`** — project evaluation orchestration\n- **`ItemSpec.cs` / `LazyItemEvaluator.cs`** — item evaluation and globbing\n- **`TaskExecutionHost.cs`** — task parameter marshaling\n- **File I/O paths** — `FileMatcher.cs`, glob operations, project loading\n\n### Inlining Considerations\n\nFor extremely hot methods, consider:\n- `[MethodImpl(MethodImplOptions.AggressiveInlining)]` for small methods called millions of times\n- Avoiding virtual dispatch in inner loops\n- Using `struct` enumerators to avoid heap allocation\n\n## Evaluation Performance Specifics\n\n- **Import chain depth** affects evaluation time linearly. Minimize unnecessary imports.\n- **Glob patterns** are evaluated per-project. Overly broad globs (`**/*`) are expensive.\n- **Condition evaluation** should use short-circuit logic — put cheap checks first.\n- **Property functions** (`$([System.IO.Path]::...)`) are interpreted and slower than built-in operations.\n\n## Anti-Patterns\n\n| Anti-Pattern | Why It's Bad | Fix |\n|-------------|-------------|-----|\n| `items.Count() > 0` | Enumerates entire collection | `items.Any()` or check `.Count` property |\n| `string.Format` in log messages at Low importance | Allocates even if message is filtered | Use structured logging or guard with verbosity check |\n| `new List<T>(enumerable).ToArray()` | Double allocation | `enumerable.ToArray()` directly |\n| `dict.ContainsKey(k) then dict[k]` | Double lookup | `dict.TryGetValue(k, out var v)` |\n| Regex in a loop without `RegexOptions.Compiled` | Reinterprets pattern each time | Compile or use static `Regex` field |\n",".github/skills/pipelines-health-check/SKILL.md":"---\nname: pipelines-health-check\ndescription: Check health of MSBuild CI pipelines, VS repo PR insertion statuses, and VMR codeflow PRs. Use when asked about pipeline health, build failures, infrastructure issues, CI status, insertion PR status, VMR codeflow status, or for periodic health monitoring.\n---\n\n# Pipelines & PR Health Check\n\nThis skill checks the health of MSBuild's CI pipelines, the status of insertion PRs in the VS repository, and the health of VMR codeflow PRs that flow MSBuild source into the dotnet/dotnet unified repository.\n\n## When to Use\n\n- User asks about MSBuild pipeline health, CI status, or build failures\n- User asks about VS insertion PR status or whether insertions are going through\n- User asks about VMR codeflow PRs, dotnet/dotnet PR status, or dotnet-unified-build pipeline\n- User asks to check if there are failing checks on PRs\n- User asks for a health check or status overview\n- Periodic monitoring requests\n\n## Prerequisites\n\n- `az` CLI must be installed and authenticated (`az login` with access to the DevDiv organization)\n- Azure DevOps extension for `az` must be installed: `az extension add --name azure-devops`\n- PowerShell 5.1+ or PowerShell Core\n\n### Optional: WorkIQ (for infrastructure issue investigation)\n\n[WorkIQ](https://www.npmjs.com/package/@microsoft/workiq) is an MCP server / CLI that can query Microsoft 365 data (people, emails, Teams, documents) to find **service ownership, contacts, and incident context** when pipeline failures are caused by infrastructure issues outside MSBuild's control.\n\n**Check availability:**\n```powershell\nworkiq version\n# Expected: 0.2.x or later\n```\n\n**If not installed**, set it up:\n```powershell\n# Install globally (use --registry if your .npmrc redirects @microsoft scope to GitHub Packages)\nnpm install -g @microsoft/workiq --registry https://registry.npmjs.org\n\n# Accept the EULA (required once)\nworkiq accept-eula\n```\n\nWorkIQ is not required for the core health check. If unavailable, the skill will still work — it will simply skip the ownership lookup and suggest manual investigation or offer to help install WorkIQ.\n\n## Reference Information\n\n### Pipelines\n\n| Pipeline | Org/Project | ID | Purpose |\n|----------|-------------|----|---------|\n| MSBuild | devdiv/DevDiv | 9434 | Main CI pipeline — builds and tests on every commit to main |\n| MSBuild-OptProf | devdiv/DevDiv | 17389 | Optimization/profiling pipeline — runs on schedule |\n| dotnet-unified-build | dnceng-public/public | — | VMR build pipeline — runs on codeflow PRs in dotnet/dotnet |\n\n### Key URLs\n\n- MSBuild pipeline: `https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434`\n- OptProf pipeline: `https://devdiv.visualstudio.com/DevDiv/_build?definitionId=17389`\n- VS PRs assigned to MSBuild: `https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequests?_a=active&assignedTo=66cc9d27-aef7-4399-ba2c-3dccb4489098`\n- VMR codeflow PRs: `https://github.com/dotnet/dotnet/pulls?q=is:pr+is:open+\"Source+code+updates+from+dotnet/msbuild\"`\n- dotnet-unified-build (public): `https://dev.azure.com/dnceng-public/public/_build`\n\n## Phase 1: Collect Data & Present Overview Table\n\n### Step 1: Run all data collection scripts\n\nRun these three scripts from the repository root **in parallel**. They output JSON to stdout. Each script may take 1–3 minutes depending on the number of PRs and pipeline runs to fetch, so use an `initial_wait` of **120 seconds** or higher.\n\n```powershell\n# Pipeline health (checks both MSBuild and MSBuild-OptProf)\n$pipelineJson = & .\\.github\\skills\\pipelines-health-check\\check-pipeline-health.ps1\n\n# VS PR status (checks active non-Experimental PRs and last merged PR)\n$prJson = & .\\.github\\skills\\pipelines-health-check\\check-vs-pr-status.ps1\n\n# VMR codeflow status (checks open codeflow PRs from msbuild and their dotnet-unified-build runs)\n$vmrJson = & .\\.github\\skills\\pipelines-health-check\\check-vmr-codeflow.ps1\n```\n\nAll scripts write progress messages to stderr (`Write-Host`) and the JSON result to stdout.\n\nThe scripts sanitize error messages (stripping control characters, truncating to 500 chars) so the JSON output can be parsed directly with `ConvertFrom-Json` without additional cleanup.\n\n### Step 2: Present the overview table IMMEDIATELY\n\nParse the JSON outputs and render status overview tables to the user **before** doing any deeper investigation. This gives the user instant visibility.\nPresent ALL tables - for both pipelines and for the VS insertion PRs. Do not omit any of those unless explicitly asked by user just for some specific overview.\n\n#### Pipeline Health Table\n\nFor each pipeline in the JSON output, render one row:\n\n| Pipeline | Last Success | Age | Recent Runs | Status |\n|----------|-------------|-----|-------------|--------|\n| {pipelineName} ({pipelineId}) | {lastSuccessfulRun.finishTime} | {lastSuccessfulRun.ageHours}h | emoji sequence | status emoji + label |\n\n**Recent Runs column:** Show an emoji for each run in `recentRuns` array (newest first):\n- `✅` for `succeeded`\n- `❌` for `failed`\n- `⏳` for `inProgress`\n- `⚪` for `canceled` or other\n\n**Status column** — derive from `healthSummary` and `lastSuccessfulRun.ageHours`:\n- `✅ HEALTHY` — healthSummary starts with \"HEALTHY\"\n- `⚠️ FLAKY` — healthSummary starts with \"FLAKY\"\n- `🔴 UNHEALTHY` — healthSummary starts with \"UNHEALTHY\"\n- Add `⚠️` if ageHours > 24, `🔴` if ageHours > 48 (even if some runs succeed, stale success is a concern)\n\n#### VS Insertion PRs Table (non-Experimental)\n\nFor each PR in the `prs` array:\n\n| PR | Title | Checks ✅ | Checks ⏳ | Checks ❌ | Status |\n|----|-------|-----------|-----------|-----------|--------|\n| [{id}](url) | {title} (truncated) | {checks.succeeded} | {checks.pending} | {checks.failed} | status |\n\n**Status column:**\n- `🔴 Failing` — if `actionNeeded` is true (has failed required checks)\n- `⏳ Running` — if `checks.pending > 0` and no failures\n- `✅ Green` — if all checks succeeded or notApplicable\n\n#### Last Merged Insertion Row\n\n| Last Merged PR | Date | Age | Status |\n|---------------|------|-----|--------|\n| [{lastMergedPr.id}](lastMergedPr.url) | {lastMergedPr.closedDate} | {ageDays} days | status |\n\n**Status:**\n- `✅ Recent` — ageHours ≤ 48 (≤ 2 business days)\n- `⚠️ Getting stale` — ageHours > 48 and ≤ 96\n- `🔴 Stale insertion` — ageHours > 96 (> 4 business days)\n\n**Note on weekends:** When computing business-day age, be aware that weekends inflate the hour count. If today is Monday and the last merge was Friday, that's ~72h but only 1 business day. Mention this nuance to the user if the age seems borderline.\n\n#### VMR Codeflow PRs Table\n\nFor each codeflow PR in the `codeflowPRs` array from `$vmrJson`:\n\n| Codeflow PR | Age | Pipeline Runs | Upstream PRs | Status |\n|-------------|-----|---------------|--------------|--------|\n| [#{prNumber}](prUrl) | {prAge}h | emoji sequence from pipelineRuns | count | status emoji + label |\n\n**Pipeline Runs column:** Show an emoji for each run in `pipelineRuns` (newest first):\n- `✅` for `result == \"succeeded\"`\n- `❌` for `result == \"failed\"`\n- `⏳` for `status == \"inProgress\"`\n- `⚪` for other/no runs\n\n**Status column** — derive from `healthSummary`:\n- `✅ HEALTHY` — healthSummary starts with \"HEALTHY\"\n- `🔄 IN PROGRESS` — healthSummary starts with \"IN_PROGRESS\" or \"RETRYING\"\n- `🔴 FAILING` — healthSummary starts with \"UNHEALTHY\"\n- `⚠️ MIXED` — healthSummary starts with \"MIXED\"\n- `❓ UNKNOWN` — no pipeline runs found\n\n**If a PR has failures**, also render a failure details sub-table:\n\n| Failed Job | Failed Task | Error Category | Related Upstream PRs |\n|------------|------------|----------------|---------------------|\n| {job.name} | {task.name} | {category} | PR links from failureCorrelation |\n\nThe `failureCorrelation` array maps each failed build to error categories and potentially related upstream PRs (matched by title keywords). This helps quickly identify which msbuild change likely caused a VMR build failure.\n\n**Upstream PRs list:** For each codeflow PR, list the included upstream msbuild PRs:\n\n> **Included changes:** [#13175](url) Add App Host Support, [#13306](url) IBuildEngine callbacks, ...\n\n### Step 3: Identify problems\n\nAfter rendering the table, build a list of distinct problems. A \"problem\" is any of:\n\n1. **Pipeline failure** — A pipeline whose latest run on main failed, especially if `lastSuccessfulRun.ageHours > 24`\n2. **PR check failure** — An active non-Experimental PR that has `actionNeeded: true` (failed required checks)\n3. **Stale insertion** — `lastMergedPr.ageHours > 48` (no successful insertion in >2 business days)\n4. **All checks pending** — A PR where all checks are still pending/queued (may indicate a stuck pipeline or queue issue)\n5. **VMR codeflow failure** — A codeflow PR whose `healthSummary` starts with \"UNHEALTHY\" (dotnet-unified-build failing)\n6. **VMR codeflow stale** — A codeflow PR older than 48 hours with no successful pipeline run\n\nIf there are **no problems**, report `✅ ALL CLEAR — pipelines healthy, PRs on track, insertions flowing, VMR codeflow green` and stop. Do not proceed to Phase 2.\n\n## Phase 2: Investigate Problems via Subagents\n\nFor **each distinct problem** identified in Step 3, launch a **separate subagent** to perform DEEP, DETAILED investigation (use `#tool:agent/runSubagent` to run the investigation tasks). Fire them in parallel when possible. Use the <agent> template below to seed them. \n\n<subagent>\n\n### Subagent prompt templates\n\n#### For pipeline failures\n\n```\nInvestigate why the Azure DevOps pipeline \"{pipelineName}\" (ID: {pipelineId}) is failing.\n\nRecent failed runs on branch {branch}:\n{for each failed run, list: Run ID, start time, URL, and the failedTasks with their error messages}\n\nLast successful run: {lastSuccessfulRun.finishTime} ({ageHours} hours ago)\nURL: {lastSuccessfulRun.url}\n\nTasks:\n1. Categorize each failure as one of:\n   - BUILD ERROR: compilation failures, test failures, task execution errors in MSBuild code\n   - CONFIG/PERMISSION: signing errors, NuGet authentication, certificate issues, feed access\n   - INFRA/TRANSIENT: errors indicating unavailability or outage of services or resources\n2. Check if all recent failures share the same root cause or if there are different issues\n3. If infra/transient: suggest retrying the pipeline (provide the pipeline URL)\n4. If build error: \n  - Check the `For build errors` section below on how to investigate build errors with binlogs\n  - identify which component/task is failing and check recent commits to main to try to identify offending one.\n5. If infrastructure issues:\n  - Try to distill the exact reason for the issue, check if there are other failing pipelines with the same issue or any open bugs for the issue.\n  - **Use WorkIQ** to find the owning team and contacts. Check if `workiq` CLI is available (`workiq version`).\n    - If available, run: `workiq ask -q \"Who owns the {failing service/task name} service in Microsoft DevDiv? Who should be contacted about {brief error description}?\"`\n    - Include the WorkIQ response in your findings — it typically returns team names, distribution lists, contact people, and escalation paths.\n    - You can also ask WorkIQ about known outages: `workiq ask -q \"Are there any known outages or incidents for {service name} in Azure DevOps?\"`\n    - If WorkIQ is NOT available, note this in your report and suggest the user install it:\n      ```\n      npm install -g @microsoft/workiq --registry https://registry.npmjs.org\n      workiq accept-eula\n      ```\n  - Put together a concise overview of the issue, along with links to the failure messages, the owning team/contacts from WorkIQ, and suggested next steps.\n\nReturn: A comprehensive root cause analysis with category, explanation, links to failure messages, ownership info (from WorkIQ if available), and recommended action.\n```\n\n#### For PR check failures\n\n```\nInvestigate failing checks on VS insertion PR #{prId}: \"{prTitle}\"\nPR URL: {prUrl}\n\nFailed checks:\n{for each item in checks.failedChecks: genre, name, description, isRequired}\n\nPending checks (still running):\n{for each item in checks.pendingChecks: genre, name, description, isRequired}\n\nPipeline health context:\n{brief summary of pipeline health from Phase 1 — are pipelines also failing?}\n\nTasks:\n1. Identify which failed checks are required vs optional\n2. If required checks are failing, determine if this could be related to pipeline failures (same root cause)\n3. If checks are just pending/queued, note that they may still be running and suggest waiting\n4. Recommend specific actions: retry checks, investigate pipeline, or wait\n5. If check is failing - try to traverse the chain of called pipelines to the actual error, then: \n  - Check the `For build errors` section below on how to investigate build errors with binlogs\n  - identify which component/task is failing and check recent commits to msbuild main to try to identify offending one.\n\nReturn: Which checks need attention, likely cause, and recommended action.\n```\n\n#### For stale insertion\n\n```\nInvestigate why MSBuild insertions into VS appear stale.\n\nLast successfully merged non-Experimental PR: #{lastMergedPr.id} \"{lastMergedPr.title}\"\n  Merged: {lastMergedPr.closedDate} ({ageDays} days ago)\n  URL: {lastMergedPr.url}\n\nCurrently active non-Experimental PRs:\n{for each PR: id, title, url, checks summary, actionNeeded}\n\nPipeline health:\n{brief pipeline health summary}\n\nTasks:\n1. Check if there are active non-Experimental PRs waiting — if none, the issue may be that no insertion was triggered\n2. If there are active PRs with failing checks, identify if those failures are blocking the insertion\n3. If there are active PRs with all checks pending, they may just need time\n4. Correlate with pipeline health — if the CI pipeline is broken, insertions can't succeed\n5. Recommend specific actions to unblock\n\nReturn: Explanation of why insertion appears stuck and what to do about it.\n```\n\n#### For VMR codeflow failures\n\n```\nInvestigate failing dotnet-unified-build pipeline for VMR codeflow PR #{prNumber}: \"{prTitle}\"\nPR URL: {prUrl}\nPR Branch: {prBranch}\n\nThis is a codeflow PR that brings MSBuild source changes into the dotnet/dotnet VMR (Virtual Monolithic Repository).\nThe dotnet-unified-build pipeline runs in the dnceng-public/public Azure DevOps org.\n\nFailed pipeline runs:\n{for each failed run in pipelineRuns: buildId, buildNumber, URL, stages, failedJobs with their failedTasks and errors}\n\nFailure categories detected: {failureCorrelation[].categories}\n\nUpstream MSBuild PRs included in this codeflow:\n{for each PR in upstreamPRs: number, title, url, merged status}\n\nFailure-to-change correlation (from script):\n{for each item in failureCorrelation: buildId, categories, relatedUpstreamPRs}\n\nTasks:\n1. For each failed job, examine the error messages and categorize:\n   - TASK_HOST: MSB4216 errors about MSBuild not finding the task host executable — typically caused by changes to MSBuild's app host, node launching, or SDK layout\n   - SOURCE_BUILD_TASK_HOST: Same as TASK_HOST but in the source-only build (CentOS offline) — the previously-source-built SDK doesn't have the expected MSBuild executable\n   - COMPILATION: CS/VB/FS compilation errors — a code change broke the build\n   - BUILD_COMMAND: MSB3073 \"exited with code N\" — a build script or test failed\n   - NUGET_AUTH/SIGNING/TIMEOUT/RESOURCE: Infrastructure issues\n2. Use the failure-to-change correlation to identify which upstream MSBuild PR most likely caused each failure:\n   - Check if the error is in an area touched by one of the upstream PRs\n   - If TASK_HOST errors: look for PRs touching NodeLauncher, NodeProvider, task host, app host, or BuildEnvironmentHelper\n   - If COMPILATION errors: check which files the upstream PRs modified and whether any could cause the compilation break\n   - If test failures: identify which test is failing and which upstream PR likely affects that code path\n3. Check if there is an in-progress retry build that might resolve the issue\n4. If the failure appears to be an infrastructure issue (not caused by MSBuild changes), note that\n5. For build errors, check the `For build errors` section on how to investigate with binlogs\n6. Recommend specific actions:\n   - If an upstream PR is clearly at fault, suggest reverting it or filing a fix\n   - If a VMR-side fix is needed (e.g., SDK layout change), describe what needs to change\n   - If a retry might help (transient infra), suggest re-running the pipeline\n   - If the issue is already being retried (in-progress build), suggest waiting\n\nReturn: Root cause analysis mapping each failure to the likely upstream PR, with explanation, links, and recommended action.\n```\n\n#### For build errors\n\nTasks:\n1. Try to find a .binlog file(s) in the build or step artifacts and fetch it\n2. Ensure to acquire the [binlog-failure-analysis skill](https://github.com/ViktorHofer/dotnet-skills/blob/main/msbuild-skills/skills/binlog-failure-analysis/SKILL.md) together with the binlog-mcp (spawn via `dnx -y baronfel.binlog.mcp@0.0.13`)\n3. Use the binlog analysis skill and mcp to analyse the binlog(s) you found and analyse problems from those\n\n</subagent>\n\n## Phase 3: Final Report\n\nAfter all subagent results return, present the findings below the overview table under a **\"🔍 Problems & Recommendations\"** heading. For each problem:\n\n```markdown\n### Problem: {brief title}\n**Category:** {INFRA | BUILD | CONFIG | PR_CHECKS | STALE_INSERTION | VMR_CODEFLOW}\n**Details:** {subagent's explanation}\n**Ownership:** {owning team, contacts, DL from WorkIQ — include only for INFRA/CONFIG issues}\n**Recommended Action:** {subagent's recommendation}\n```\n\nIf all problems are infra/transient, add a note: *\"All current issues appear to be infrastructure-related. Consider retrying the pipelines and checking again in 30 minutes.\"*\n\n## Troubleshooting\n\n### \"az: command not found\" or \"az account get-access-token\" fails\nThe `az` CLI is not installed or not authenticated. Run `az login` first.\n\n### Scripts return empty arrays\n- Check that you have access to the DevDiv organization\n- The branch filter defaults to `main` — if checking a different branch, pass `-Branch <name>` to the pipeline script\n\n### PR statuses all show as \"pending\"\nThis is normal for newly created PRs. The checks take time to queue and run. If checks are pending for more than a few hours, this may indicate a stuck pipeline or queue issue.\n\n### Timeout or rate limiting\nIf the scripts take a long time or fail with 429 errors, Azure DevOps may be rate-limiting. Wait a minute and retry.\n\n### WorkIQ not found or EULA not accepted\nIf `workiq version` fails, install it:\n```powershell\nnpm install -g @microsoft/workiq --registry https://registry.npmjs.org\nworkiq accept-eula\n```\nNote: If your `.npmrc` redirects the `@microsoft` scope to GitHub Packages, use `--registry https://registry.npmjs.org` to override, or pass `--userconfig` pointing to a clean `.npmrc`.\n\n### WorkIQ returns empty or unhelpful results\nWorkIQ queries Microsoft 365 data (Outlook, Teams, SharePoint). Results depend on your account's access and the data available in your tenant. Try rephrasing the question or being more specific about the service name. Example queries that work well:\n- `workiq ask -q \"Who owns the MicroBuild service in Microsoft?\"`\n- `workiq ask -q \"Who owns the CloudBuild signing service in DevDiv?\"`\n- `workiq ask -q \"Who should I contact about NuGet feed authentication failures in Azure DevOps?\"`\n\n### VMR codeflow script returns no PRs\n- Verify there are actually open codeflow PRs: check `https://github.com/dotnet/dotnet/pulls?q=is:pr+is:open+\"Source+code+updates+from+dotnet/msbuild\"`\n- Verify `gh` CLI is authenticated: run `gh auth status`\n\n### VMR pipeline runs not found\n- The dotnet-unified-build pipeline runs in `dnceng-public/public`, which is publicly accessible\n- If no runs appear, the pipeline may not have been triggered yet for that PR\n- Codeflow PRs may take a few minutes after creation before CI triggers\n\n### JSON output too large or contains unexpected characters\n- Error messages from Azure DevOps timelines can contain Windows paths, control characters, and multi-KB stack traces\n- The scripts sanitize and truncate these to 500 characters — if you still see issues, check that you're running the latest version of the scripts\n- Use `ConvertFrom-Json` in PowerShell or `json.loads()` in Python to parse the output; avoid manual string manipulation\n",".github/skills/project-management/SKILL.md":"---\nname: project-management\ndescription: 'GitHub issue and project-board management for the dotnet/msbuild repo. Use when asked to file/triage/update issues, post comments, amend issue bodies, move sprints, bulk-update project board fields, or audit items by sprint/status/assignee.'\nargument-hint: 'Manage issues and project board entries for dotnet/msbuild.'\n---\n\n# Project Management for dotnet/msbuild\n\nThis skill covers everything you need to manage issues and the project board for the MSBuild repository: querying, commenting, editing, creating issues, and bulk operations on the project board (sprints, status, assignees, etc.).\n\n## Where things live\n\n- **Repository:** `dotnet/msbuild` — all issues, PRs, and discussions.\n- **Project board:** [`dotnet/projects/117`](https://github.com/orgs/dotnet/projects/117) — title \"MSBuild\". This is the canonical project board for the team. Most issues created in `dotnet/msbuild` are auto-added or manually added here for sprint planning.\n- **Sprints** are tracked via the **Sprint** iteration field on project 117. Iteration titles use the format `2026 May I`, `2026 May II`, `2026 June I`, etc. (roughly two-week iterations).\n\n> Note: there are/were other auxiliary project boards for specific initiatives. Use project 117 unless the user explicitly points at another one.\n\n### Stable identifiers for project 117\n\n| Identifier | Value |\n|---|---|\n| Project node ID | `PVT_kwDOAIt-yc4ABM5F` |\n| Sprint field ID (iteration) | `PVTIF_lADOAIt-yc4ABM5FzgAtMGI` |\n\nThese IDs are stable but can be re-derived if the project is ever recreated or migrated:\n```bash\n# Project node ID\ngh api graphql -f query='{ organization(login:\"dotnet\") { projectV2(number:117) { id } } }'\n\n# All field IDs (Sprint, Status, Area, Priority, …)\ngh project field-list 117 --owner dotnet --format json | jq '.fields[] | {id,name,type}'\n```\n\n## Tooling preference\n\nFor GitHub work, prefer `gh` CLI over MCP tools (per repo convention).\n\n- Use `gh issue ...` for issue-level operations (create / view / comment / edit / close).\n- Use `gh project ...` for project-board CRUD.\n- Use `gh api graphql` for anything `gh` doesn't expose directly (sprint values per item, project-level field values, bulk reads).\n\nThe default `gh` token may not include the `project` scope. If a `gh project ...` write call fails with a scope error, ask the user to run:\n```bash\ngh auth refresh -s project\n```\n\n## Common single-issue actions\n\n```bash\n# View\ngh issue view 13315 --repo dotnet/msbuild --json title,body,state,assignees,labels\n\n# Comment (use --body-file to avoid shell-quoting traps with multi-line markdown)\ngh issue comment 13315 --repo dotnet/msbuild --body-file ./comment.md\n\n# Amend body (overwrite — there is no append; use view → edit → write)\ngh issue edit 13597 --repo dotnet/msbuild --body-file ./new-body.md\n\n# Create\ngh issue create --repo dotnet/msbuild --title \"Title\" --body-file ./body.md\n\n# Add labels / assignees\ngh issue edit 13315 --repo dotnet/msbuild --add-label \"Area: Server\" --add-assignee someuser\n```\n\nWhen drafting comments / bodies that summarize internal discussions:\n- Use neutral, team-voice language (\"the team agreed\", \"open question\").\n- Scrub names, \"X said Y\", room/agent identifiers, and anything resembling internal politics.\n- Always write to a file and use `--body-file`. Don't try to inline multi-line markdown.\n\n## Querying issue → project field values (sprint, status, project-level assignees)\n\n`gh issue view` does **not** return project-board field values. Use GraphQL via the `repository → issue → projectItems` path:\n\n```bash\ngh api graphql -f query='\nquery {\n  r: repository(owner:\"dotnet\", name:\"msbuild\") {\n    issue(number: 13315) {\n      id\n      projectItems(first: 5) {\n        nodes {\n          id\n          project { number title }\n          sprint:  fieldValueByName(name: \"Sprint\")  { ... on ProjectV2ItemFieldIterationValue   { title iterationId } }\n          status:  fieldValueByName(name: \"Status\")  { ... on ProjectV2ItemFieldSingleSelectValue { name optionId } }\n        }\n      }\n    }\n  }\n}'\n```\n\n> `fieldValueByName` returns `null` when the field has no value set on that item — handle accordingly.\n\n## Bulk / board-wide queries\n\nThe `gh project item-list` CLI command has internal limits and is unreliable for large boards (project 117 is in the thousands of items). **Use GraphQL with pagination.**\n\n```python\nimport subprocess, json\n\nPROJECT_NUMBER = 117\nORG = \"dotnet\"\n\nresults = []\ncursor = None\nwhile True:\n    after = f', after: \"{cursor}\"' if cursor else ''\n    query = f'''query {{\n      organization(login: \"{ORG}\") {{\n        projectV2(number: {PROJECT_NUMBER}) {{\n          items(first: 100{after}) {{\n            pageInfo {{ hasNextPage endCursor }}\n            nodes {{\n              id\n              fieldValues(first: 20) {{\n                nodes {{\n                  ... on ProjectV2ItemFieldIterationValue {{\n                    field {{ ... on ProjectV2IterationField  {{ name }} }}\n                    title\n                  }}\n                  ... on ProjectV2ItemFieldUserValue {{\n                    field {{ ... on ProjectV2Field           {{ name }} }}\n                    users(first: 10) {{ nodes {{ login }} }}\n                  }}\n                  ... on ProjectV2ItemFieldSingleSelectValue {{\n                    field {{ ... on ProjectV2SingleSelectField {{ name }} }}\n                    name\n                  }}\n                }}\n              }}\n              content {{\n                ... on Issue        {{ number title }}\n                ... on PullRequest  {{ number title }}\n                ... on DraftIssue   {{ title }}\n              }}\n            }}\n          }}\n        }}\n      }}\n    }}'''\n    try:\n        r = subprocess.run(['gh','api','graphql','-f',f'query={query}'],\n                           capture_output=True, text=True, check=True)\n    except subprocess.CalledProcessError as e:\n        raise RuntimeError(f\"GraphQL query failed: {e.stderr}\") from e\n    page = json.loads(r.stdout)['data']['organization']['projectV2']['items']\n    for node in page['nodes']:\n        sprint = status = None\n        assignees = []\n        for fv in node.get('fieldValues', {}).get('nodes', []):\n            if not fv:                              # null entries for unset fields\n                continue\n            fname = (fv.get('field') or {}).get('name', '')\n            if fname == 'Sprint':\n                sprint = fv.get('title')\n            elif fname == 'Status':\n                status = fv.get('name')\n            elif fname == 'Assignees':\n                assignees = [u['login'] for u in (fv.get('users') or {}).get('nodes', [])]\n        results.append({'id': node['id'], 'sprint': sprint, 'status': status,\n                        'assignees': assignees, 'content': node.get('content')})\n    if not page['pageInfo']['hasNextPage']:\n        break\n    cursor = page['pageInfo']['endCursor']\n```\n\n## Updating project-board field values\n\n### Set a sprint (iteration) — first look up iteration IDs\n\nIteration IDs are short hex strings (e.g., `303c2425`). They are **not** the same across projects; always look them up from project 117.\n\n```bash\ngh api graphql -f query='\nquery {\n  node(id: \"PVTIF_lADOAIt-yc4ABM5FzgAtMGI\") {\n    ... on ProjectV2IterationField {\n      configuration {\n        iterations           { id title startDate }\n        completedIterations  { id title startDate }\n      }\n    }\n  }\n}' | jq '.data.node.configuration | (.iterations + .completedIterations)[] | {id,title}'\n```\n\n### Single-item field update — `gh project item-edit`\n\n```bash\n# Iteration / sprint\ngh project item-edit \\\n  --project-id PVT_kwDOAIt-yc4ABM5F \\\n  --id <PROJECT_ITEM_ID> \\\n  --field-id PVTIF_lADOAIt-yc4ABM5FzgAtMGI \\\n  --iteration-id <ITERATION_ID>\n\n# Single-select (Status, Area, Priority): use --single-select-option-id\n# Text: --text  | Number: --number  | Date: --date YYYY-MM-DD\n```\n\n### Single-item field update — GraphQL mutation (works the same)\n\n```bash\ngh api graphql -f query='\nmutation {\n  updateProjectV2ItemFieldValue(input: {\n    projectId: \"PVT_kwDOAIt-yc4ABM5F\",\n    itemId:    \"<PROJECT_ITEM_ID>\",\n    fieldId:   \"PVTIF_lADOAIt-yc4ABM5FzgAtMGI\",\n    value:     { iterationId: \"<ITERATION_ID>\" }\n  }) { projectV2Item { id } }\n}'\n```\n\n### Adding a freshly created issue to project 117\n\nA newly-created issue is not always auto-added. To add and set its sprint:\n\n```bash\nISSUE_NODE_ID=$(gh api graphql -f query='\n  query { repository(owner:\"dotnet\",name:\"msbuild\") { issue(number:13707) { id } } }' \\\n  | jq -r '.data.repository.issue.id')\n\nITEM_ID=$(gh api graphql -f query='\n  mutation { addProjectV2ItemById(input:{\n      projectId:\"PVT_kwDOAIt-yc4ABM5F\",\n      contentId:\"'\"$ISSUE_NODE_ID\"'\"\n  }) { item { id } } }' | jq -r '.data.addProjectV2ItemById.item.id')\n\ngh api graphql -f query='\n  mutation { updateProjectV2ItemFieldValue(input:{\n      projectId:\"PVT_kwDOAIt-yc4ABM5F\",\n      itemId:\"'\"$ITEM_ID\"'\",\n      fieldId:\"PVTIF_lADOAIt-yc4ABM5FzgAtMGI\",\n      value:{iterationId:\"<ITERATION_ID>\"}\n  }) { projectV2Item { id } } }'\n```\n\n## Common errors and gotchas\n\n### 1. `Field 'fieldValueByName' has an argument conflict`\n\n`gh api graphql --paginate` (and several jq-style aliases) re-uses field names. If you call `fieldValueByName(name:\"Sprint\")` and `fieldValueByName(name:\"Status\")` in the same selection set without aliases, you'll get:\n\n```\nField 'fieldValueByName' has an argument conflict: {name:\"Sprint\"} or {name:\"Status\"}?\n```\n\nFix: alias each call.\n\n```graphql\nsprint: fieldValueByName(name: \"Sprint\")  { ... }\nstatus: fieldValueByName(name: \"Status\")  { ... }\n```\n\n### 2. `gh api graphql --paginate` produces concatenated JSON, not one document\n\nEach page is emitted as a separate JSON object on stdout; piping straight into `jq` fails with \"Invalid numeric literal\". Either:\n- paginate manually in a loop (preferred for large reads), or\n- use `jq --slurp` to read the stream as an array.\n\n### 3. \"Selections can't be made directly on unions\"\n\n`field` on a project field value is a union; you can't select `name` directly. Use a typed inline fragment per concrete field type (`ProjectV2Field`, `ProjectV2IterationField`, `ProjectV2SingleSelectField`, …).\n\n### 4. Issue-level assignees ≠ project-board \"Assignees\" column\n\nThe board's \"No Assignees\" slice is driven by the **project-level Assignees field**, not by `Issue.assignees`. They can diverge. When filtering by board state, always read `ProjectV2ItemFieldUserValue` from `fieldValues`.\n\n### 5. `null` entries in `fieldValues.nodes`\n\nGraphQL returns `null` for items where a particular field is unset. Always guard `if not fv: continue` before dereferencing.\n\n### 6. DraftIssue items lack `number` / `repository`\n\n`content` may be a `DraftIssue` that only exposes `title`, `body`, `assignees`. Code that joins to issue numbers must handle `None`.\n\n### 7. `gh project item-list` is unreliable for large boards\n\nIt silently truncates. Always use the GraphQL pagination loop above instead.\n\n### 8. Missing `project` scope on `gh` token\n\nReads work; writes (`gh project item-edit`, the `addProjectV2ItemById` / `updateProjectV2ItemFieldValue` mutations) fail. Have the user run `gh auth refresh -s project`.\n\n### 9. `gh issue edit --body` overwrites — there is no append\n\nTo \"append\" to a body, fetch with `gh issue view --json body`, append, write back with `--body-file`. For status updates, prefer a comment.\n\n## Sanitization checklist for comments / body edits\n\nWhen summarizing meetings or chat threads into GitHub:\n\n- ✅ Use team voice (\"the team agreed\", \"open question\", \"follow-up needed\").\n- ✅ Keep objective, decision-relevant content (problem statement, scope, exit criteria, repro steps, numbers).\n- ❌ No personal names or \"X said / Y replied\".\n- ❌ No full email addresses of external contributors (use GitHub handles instead).\n- ❌ No internal-only identifiers (room names, recording IDs, agent fleet identifiers, partner-team contractor names).\n- ❌ No speculation about other teams' motives or anything resembling internal politics.\n- ❌ No confidential roadmap dates or unreleased product details unless already public.\n- Always show the user the proposed comment / body before posting (unless they've explicitly preapproved).\n\n## Common workflows\n\n| Goal | Sketch |\n|---|---|\n| Add a status comment to N issues | `for n in 13315 13702 …; do gh issue comment $n --repo dotnet/msbuild --body-file ./c-$n.md; done` |\n| Replace a one-line issue body with a full Context / Exit-criteria / Out-of-scope spec | `gh issue edit <n> --repo dotnet/msbuild --body-file ./body.md` |\n| Move all unassigned items from sprint X to sprint Y | GraphQL pagination loop above + `gh project item-edit --iteration-id` per item |\n| File a new issue and put it in the current sprint | `gh issue create` → `addProjectV2ItemById` → `updateProjectV2ItemFieldValue` |\n| Audit \"what's in the current sprint that has no owner\" | GraphQL pagination loop, filter on `Sprint == \"<current>\"` and empty `Assignees` |\n",".github/skills/release/SKILL.md":"---\nname: release\ndescription: 'Orchestrate an MSBuild release: create the tracking issue, branch, configure DARC channels and subscriptions, bump version in main, final-brand the release branch, insert into VS, and publish post-GA. Covers the full monthly release lifecycle aligned with VS shipping cadence.'\nargument-hint: 'Specify which phase to execute (0-5) and provide required inputs (THIS_RELEASE_VERSION, NEXT_VERSION, etc.)'\n---\n\n# MSBuild Release Orchestration\n\nThis skill guides an agent through the MSBuild release process defined in [`documentation/release-checklist.md`](../../../documentation/release-checklist.md). The checklist is the single source of truth — this skill provides context on how to execute it.\n\n## Overview\n\nMSBuild is a **component** that gets inserted into Visual Studio. VS ships monthly; MSBuild must branch and prepare its bits **before** VS is ready to take them. See the [release process doc](../../../documentation/release.md#how-msbuild-releases-flow-into-vs) for the full timeline diagram.\n\nThe insertion pipeline routes MSBuild branches to VS branches:\n- `main` → VS `main` (daily canary)\n- `vs*` release branch → VS `main` (replaces `main` → `main` after branch snap)\n\nVS handles the progression from `main` → `rel/insiders` → `rel/stable` on its own schedule. MSBuild's responsibility is to have final-branded bits in VS `main` before the insiders snap date.\n\nEach monthly VS release produces:\n- A new `vs*` branch from `main`\n- Final branding on that branch\n- A version bump in `main`\n- DARC channel/subscription updates\n- A VS insertion\n- Post-GA publishing to nuget.org and docs\n\nThe process is organized into **6 timeline-gated phases** (0–5), each with an explicit trigger.\n\n> **Execution model**: This skill is designed for an **interactive Copilot session**. The agent walks through each phase step-by-step, but every command that modifies state (git push, DARC writes, pipeline changes, PR creation) requires **user approval** before execution. Read-only queries (DARC get-*, git log, etc.) can run without approval.\n\n## Required Inputs\n\nBefore starting any phase, ensure you have these values (the user must provide them — version increments are irregular and cannot be computed):\n\n| Input | Example | How to determine |\n|---|---|---|\n| `PREVIOUS_RELEASE_VERSION` | `18.9` | Previous release branch version: the highest `vs*` version lower than `THIS_RELEASE_VERSION` (list candidates with `git branch -r --list 'origin/vs*'`). |\n| `PREVIOUS_RELEASE_EXACT_VERSION` | `18.9.6` | The version the previous release actually shipped as. From the previous release's tracking issue, or `git tag --list 'v18.9.*'`. Used by Phase 5.3a. |\n| `THIS_RELEASE_VERSION` | `18.10` | Current `VersionPrefix` in `eng/Versions.props` (drop `.0`) |\n| `NEXT_VERSION` | `18.11` | User-provided — not computable from current version |\n| `BRANCH_SNAP_DATE` | `YYYY-MM-DD` | From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) — when MSBuild branches `vs*` from main, insertion targets VS `main` |\n| `INSIDERS_SNAP_DATE` | `YYYY-MM-DD` | From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) — when VS snaps `main` → `rel/insiders`; final-branded bits must be in VS `main` before this |\n| `STABLE_SNAP_DATE` | `YYYY-MM-DD` | From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) — when VS promotes `rel/insiders` → `rel/stable` |\n| `VS_SHIP_DATE` | `YYYY-MM-DD` | When VS ships publicly (GA) — triggers post-release tasks |\n| `PACKAGE_VALIDATION_BASELINE_VERSION` | `18.10.0-1.26378.2` | See [How to determine `PACKAGE_VALIDATION_BASELINE_VERSION`](#how-to-determine-package_validation_baseline_version) below — non-trivial: most \"obvious\" picks are wrong. |\n\n> Version examples above track the current cycle (`eng/Versions.props` `VersionPrefix` is `18.10.0`). Dates are intentionally shown as a format only — always read the real ones from the VS-Dates wiki.\n\n### How to determine `PACKAGE_VALIDATION_BASELINE_VERSION`\n\n**The value is the latest `{{THIS_RELEASE_VERSION}}.0-<label>.<shortDate>.<rev>` MSBuild package that is both:**\n\n_`<label>` is `PreReleaseVersionLabel` from `eng/Versions.props` (currently `1`), so the versions look like `18.11.0-1.26426.2`. Do **not** assume the legacy `-preview-NNNNN-NN` shape — MSBuild no longer produces it._\n\n1. **Published on the public [dotnet-tools feed](https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-tools)** — this is the feed the official build publishes to and that ApiCompat restores baselines from. If the version isn't here, ApiCompat fails with `NU1102`.\n2. **Produced from a commit reachable from `vs{{THIS_RELEASE_VERSION}}`** — i.e. a commit on `vs{{THIS_RELEASE_VERSION}}`, or the `main` commit `vs{{THIS_RELEASE_VERSION}}` was branched from.\n\n**Two tempting wrong answers — and why they're wrong:**\n\n| Wrong pick | Why it fails |\n|---|---|\n| ❌ The release-versioned `{{THIS_RELEASE_VERSION}}.X` package that ships in VS / on nuget.org | Since [#14277](https://github.com/dotnet/msbuild/pull/14277) release branches build and insert **prerelease** versions, exactly like `main`; the release-versioned variants are produced by NuGetRepack at manual publish time. They never exist on the public CI feed, so ApiCompat cannot restore them. |\n| ❌ Blindly the most recent `{{THIS_RELEASE_VERSION}}.0-*` on `dotnet-tools` | After `vs{{THIS_RELEASE_VERSION}}` branches, `main` keeps producing `{{THIS_RELEASE_VERSION}}.0-*` until **this** main-bump PR merges — so the most recent feed entries may be `{{NEXT_VERSION}}`-content builds wearing `{{THIS_RELEASE_VERSION}}` branding. Picking one drifts the API baseline forward and silently hides real compat breaks. |\n\n**Timing caveat:** the branch-point build publishes to `dotnet-tools` asynchronously. If you branch and bump on the same day, the script may report the correct candidate as `[NOT on feed]` — wait for publication and re-run rather than substituting an older build.\n\n**Procedure:** run the helper — it does the whole resolution mechanically (requires `az login` with devdiv access):\n\n```\npwsh ./scripts/Get-PackageValidationBaseline.ps1 -ThisReleaseVersion {{THIS_RELEASE_VERSION}}\n# -> prints e.g. 18.11.0-1.26426.2\n```\n\nIt computes `git merge-base origin/main origin/vs{{THIS_RELEASE_VERSION}}`, finds the matching successful build in [pipeline 9434](https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434), derives the package version from the OfficialBuildId, and verifies it on the dotnet-tools feed. If it fails, read the script's own `.DESCRIPTION` header for the manual equivalent rather than reproducing it here.\n\n### Prerequisites\n- gh cli\n- az cli\n- darc cli — Arcade enforces a minimum version; if `darc` refuses with a \"below the minimum required version\" error, run `.\\eng\\common\\darc-init.ps1`\n\n## Phase Summary\n\n| Phase | Trigger | Key Actions |\n|---|---|---|\n| **0: Instantiate** | User-initiated | Validate inputs, create GitHub tracking issue |\n| **1: Branch & Prepare** | `BRANCH_SNAP_DATE` | Create `vs*` branch, DARC channel setup (batched PR), **audit all `vs*` branches for retirement** |\n| **2: DARC Subscription Updates** | Phase 1 branch exists (`vs*` created) | Retarget `main`-targeting subs + VMR backflow to next channel, retired-branch cleanup (batched PR), Arcade verify |\n| **3: Bump Main** | Phase 2 ingested and verified | Branding PR in `main` (`VersionPrefix` → next, ApiCompat baseline, refresh OptProf baseline) |\n| **4: Final Branding** | 7 days before `INSIDERS_SNAP_DATE` | Telemetry evidence checks, package API compatibility review, OptProf bootstrap, VS insertion before Insiders snap |\n| **5: Post-GA** | VS shipped (`VS_SHIP_DATE`) | Resolve the exact shipped version (SDK-coupled? SDK wins over VS `rel/stable`), nuget.org publish, docs, GitHub release, Change Waves Learn sync, retro |\n\n## DARC Batching\n\nDARC write commands push to the [maestro-configuration](https://dev.azure.com/dnceng/internal/_git/maestro-configuration) repo. Batch related changes into **one PR**:\n\n1. Choose a branch name like `release/msbuild-{{THIS_RELEASE_VERSION}}`\n2. Add `--configuration-branch <name> --no-pr` to every write command except the last\n3. Last command: use `--configuration-branch <name>` without `--no-pr` to create the PR\n4. Review the diff before merging; disable auto-complete if the PR must remain review-only.\n5. After merging, complete [DARC configuration PR verification](../../../documentation/release-checklist.md#darc-configuration-pr-verification) before dependent release steps.\n\nCheck existing channels and mappings before writing: the previous release normally pre-created the current release's mapping. Skip satisfied writes rather than constructing a redundant batch.\n\nRead-only commands (`get-default-channels`, `get-subscriptions`, `get-channel`) don't need these flags.\n\n**Non-interactive (`-q`).** `darc add-default-channel` / `add-subscription` prompt `y/n` when the target branch does not exist yet (e.g. pre-creating the `vs{{NEXT_VERSION}}` mapping in Phase 1.2c, or adding the new `vs{{THIS_RELEASE_VERSION}}` backflow in Phase 2). `darc delete-subscriptions` always prompts for confirmation. Console input is redirected in an agent session, so the prompt **fails or hangs the command** — always pass `-q` for these writes. Note the verb is `delete-subscriptions` (plural); `delete-subscription` does not exist.\n\n**Phase 2 — forward-flow rotation.** Move subscriptions targeting `main` (`dotnet/dotnet @ main`, `dotnet/fsharp @ main`) to the next channel so those consumers receive next-version builds. Keep VMR `release/*` targets, including `.NET-next` preview bands, on `VS {{THIS_RELEASE_VERSION}}` so the release branch continues to feed its SDK bands.\n\n**Phase 2 — VMR backflow rotation.** Backflow (`dotnet/dotnet → msbuild`, source-enabled) keeps each MSBuild branch's dependencies aligned with its SDK band. When the new `vs{{THIS_RELEASE_VERSION}}` is paired with an SDK band, repoint the `→ main` backflow to the **next** SDK band channel and add backflow from the **outgoing** band channel into the release branch. Skip this for a VS-only release. See checklist steps 2.2b / 2.3f / 2.3g.\n\n## Branch retirement (Phase 1.3)\n\nAudit every release branch to keep required SDK/VS servicing flows active and remove unused dependency automation.\n\n**Branch inventory:** [Maintained branches](../../../documentation/release.md#maintained-branches) is the static list. Update its branch rows, SDK/VS sources, and support deadlines when cutting or retiring branches; cross-check the live DARC mappings, subscriptions, and repository branches in checklist step 1.3a.\n\nCheck both lifecycles because either the SDK or Visual Studio can require a branch:\n\n| Lifecycle | Source |\n|---|---|\n| **SDK band** | [supported .NET versions table](https://learn.microsoft.com/dotnet/core/porting/versioning-sdk-msbuild-vs#supported-net-versions) — maps each feature band to its VS version and support end date |\n| **Visual Studio** | [VS Product Lifecycle and Servicing](https://learn.microsoft.com/visualstudio/releases/2026/servicing-vs) — annual releases and the LTSC table |\n\n**VS 2026 and later:** non-LTSC releases are serviced through Insiders and for one month after Stable release. A new Stable promotion ends VS support for the previous release. LTSC baselines receive one year of servicing after release; use the published LTSC table to identify them.\n\nUse published SDK and VS deadlines for lifecycle decisions, and DARC channels and subscriptions for dependency routing.\n\nApply the **combined rule**: retire a branch paired with an SDK band and a VS version only when both lifecycles agree it is out of support. Check these candidates for automation cleanup:\n\n| Signal | Meaning |\n|---|---|\n| Branch's `VS X.Y` channel has **no MSBuild outbound subscription** | No downstream consumes MSBuild builds from that branch. |\n| Default channel exists for a **branch that isn't in the repo** | Pure orphan — delete the mapping. |\n\n**Retirement cleanup (2.3e):** remove inbound subscriptions, unused MSBuild outbound subscriptions, and branch-to-channel associations, including disabled subscriptions. Preserve shared channels and flows still needed by other branches or producers; verify removals after merge and ingestion.\n\n## Executing a Phase\n\nWhen asked to execute a specific phase:\n\n1. Read the full phase from `documentation/release-checklist.md`\n2. Verify the trigger condition is met (previous phases completed, including ingestion and live verification of prerequisite DARC changes)\n3. Execute steps in order — respect sequential/parallel annotations\n4. For DARC commands: batch writes into one configuration PR per phase\n5. Record all output URLs in the tracking issue's artifact table\n6. Mark checkboxes as completed in the tracking issue\n7. In **Phase 4**, complete telemetry checkpoints 4.0 and 4.4a using the shared evidence procedure in `documentation/release-checklist.md`.\n8. In **Phase 4** (step 4.7): if `documentation/wiki/ChangeWaves.md` is changed for this release, update the public Learn page at https://learn.microsoft.com/visualstudio/msbuild/change-waves. Sync the Change Waves Learn page from `documentation/wiki/ChangeWaves.md` on the `vsXX.Y` branch that is live in VS Insiders / the latest preview SDK. PR goes to `MicrosoftDocs/visualstudio-docs-pr` (`docs/msbuild/change-waves.md`); example: https://github.com/MicrosoftDocs/visualstudio-docs-pr/pull/15662.\n\n## Key Files\n\n| File | Purpose |\n|---|---|\n| [`documentation/release-checklist.md`](../../../documentation/release-checklist.md) | **Operational checklist** — the source of truth |\n| [`documentation/release.md`](../../../documentation/release.md) | Maintained-branch inventory and release process |\n| [`documentation/wiki/ChangeWaves.md`](../../../documentation/wiki/ChangeWaves.md) | Source doc for the Learn page sync — always sync the `vsXX.Y` (Insiders/preview-SDK) copy, not `main` |\n| [MSBuild Change Waves Learn page](https://learn.microsoft.com/visualstudio/msbuild/change-waves) | Public docs target to [`MicrosoftDocs/visualstudio-docs-pr`](https://github.com/MicrosoftDocs/visualstudio-docs-pr) (`docs/msbuild/change-waves.md`) |\n| [`eng/Versions.props`](../../../eng/Versions.props) | `VersionPrefix`, `PackageValidationBaselineVersion`, `BootstrapSdkVersion` |\n| [`azure-pipelines/vs-insertion.yml`](../../../azure-pipelines/vs-insertion.yml) | VS insertion pipeline — `TargetBranch` parameter and `InsertTargetBranch` routing |\n| [`azure-pipelines/vs-insertion-experimental.yml`](../../../azure-pipelines/vs-insertion-experimental.yml) | Experimental insertion — `TargetBranch` parameter values |\n| [`scripts/Get-PackageValidationBaseline.ps1`](../../../scripts/Get-PackageValidationBaseline.ps1) | Phase 3.2 — resolves `PackageValidationBaselineVersion` deterministically (merge-base → pipeline 9434 → dotnet-tools feed) |\n| [`scripts/Get-LatestOptProfDrop.ps1`](../../../scripts/Get-LatestOptProfDrop.ps1) | Phase 3.3 — resolves the latest main OptProf drop (MSBuild-OptProf pipeline 17389) to refresh `OptProfBaselineDrop` in `.vsts-dotnet.yml` |\n| [`.vsts-dotnet.yml`](../../../.vsts-dotnet.yml) | Build pipeline entry point — `OptProfBaselineDrop` (hardcoded OptProf seed for new `vs*` branches) |\n| [`azure-pipelines/.vsts-dotnet-build-jobs.yml`](../../../azure-pipelines/.vsts-dotnet-build-jobs.yml) | Build jobs — `VisualStudio.ChannelName` is release-agnostic (`int.main`); no per-release edit |\n\n## Validation\n\nAfter completing all phases, verify:\n\n1. Branch `vs{{THIS_RELEASE_VERSION}}` exists and has final branding\n2. Main has `VersionPrefix` = `{{NEXT_VERSION}}.0`\n3. DARC: main → `VS {{NEXT_VERSION}}` channel, release branch → `VS {{THIS_RELEASE_VERSION}}` channel\n4. VS insertion PR merged\n5. Packages published to nuget.org\n6. GitHub release created with tag `v{{THIS_RELEASE_EXACT_VERSION}}`\n7. The Learn page https://learn.microsoft.com/visualstudio/msbuild/change-waves lists exactly the waves present in `vsXX.Y` (the version in VS Insiders / the latest released preview SDK), or the sync is explicitly tracked\n\n## Error Recovery\n\n- **Branch already exists**: Release was partially started — check the tracking issue for progress\n- **DARC channel already exists**: Safe to continue — `add-channel` is idempotent\n- **OptProf fails on first build**: Expected — that's why we use main's OptProf data as fallback\n- **DARC configuration PR conflicts**: Rebase the configuration branch on `production` and force-push\n\n## Major Version Releases\n\nIf `NEXT_VERSION` is a new major version (e.g., 18.x → 19.0), additional steps are needed after Phase 5. See [release.md](../../../documentation/release.md) for:\n- `src/Shared/BuildEnvironmentHelper.cs` — VS major version constants\n- `src/Shared/Constants.cs` — version constants\n- `src/Framework/Telemetry/TelemetryConstants.cs` — telemetry version\n",".github/skills/reviewing-msbuild-code/SKILL.md":"---\nname: reviewing-msbuild-code\ndescription: \"Reviews MSBuild code changes using a 24-dimension methodology. Activates for code review, PR review, pull request analysis, design review, architecture review, code quality assessment, or style check of MSBuild code. Covers backwards compatibility, ChangeWave discipline, performance, allocation awareness, test coverage, error message quality, logging, string comparison, API surface, target authoring, cross-platform correctness, code simplification, concurrency, naming, SDK integration, evaluation model integrity, correctness, dependency management, security, and build infrastructure.\"\n---\n\n# MSBuild Code Review\n\nInvoke `@expert-reviewer` for 24-dimension MSBuild code review. The agent contains the full methodology, principles, dimension rules with checklists, folder hotspot mapping, and 4-wave workflow.\n",".github/skills/running-unit-tests/SKILL.md":"---\nname: running-unit-tests\ndescription: Guide for running MSBuild unit tests efficiently. Use when running, scoping, filtering, or speeding up unit tests in this repository, or when finalizing a change with a heavier validation pass. Covers xUnit v3 + Microsoft.Testing.Platform (MTP) specifics and which `dotnet test` flags do and don't apply.\nargument-hint: Run, filter, scope, or speed up MSBuild unit tests.\n---\n\n# Running MSBuild Unit Tests\n\nThis repo uses **xUnit v3** with the **Microsoft.Testing.Platform (MTP)** runner. Test projects are built as `OutputType=Exe`, so each test assembly is a self-contained host process — *not* a classic VSTest assembly.\n\nThere are **two ways to run tests**:\n\n| Method | When to use |\n|--------|-------------|\n| `dotnet test <project>` | Dev loop — run a single test project, optionally filtered |\n| `build.cmd -test` / `build.sh --test` | Final validation — builds everything and runs all test projects via the repo's Arcade harness |\n\nUse a **fast, scoped** `dotnet test` loop while iterating, and the **full `build.cmd -test`** pass before declaring the change done.\n\n## Repo-specific knobs to know about\n\nThese are configured in `Directory.Build.props` (repo root), `src/Directory.Build.targets`, and `src/Shared/UnitTests/xunit.runner.json`. They apply to both `dotnet test` and `build.cmd -test` unless noted otherwise:\n\n- **Multi-targeting**: test projects target .NETFramework and .NETCoreApp on Windows (.NETCoreApp only on Linux/macOS). `dotnet test` runs the suite **once per TFM**.\n- **Single-threaded by default**: `xunit.runner.json` sets `maxParallelThreads: 1` and `parallelizeTestCollections: false`. Many tests mutate process-global state (env vars, cwd, SDK resolvers), so this is intentional.\n- **Auto trait filters**: platform/TFM-inappropriate tests are filtered out via `--filter-not-trait Category=...` (e.g., `nonwindowstests`, `failing`, `nonnetcoreapptests`). Don't try to \"fix\" tests that appear skipped because of these.\n- **Coverage on non-Windows**: `--coverage --coverage-settings Coverage.config` is appended unconditionally to `XunitOptions` in `src/Directory.Build.targets`. There is no MSBuild property switch to disable it from `dotnet test` — to skip coverage, run the test exe directly without `--coverage`.\n- **Test runner**: `TestRunnerName=XUnitV3`, MTP `1.9.1`, xUnit v3 `3.2.2` (set in repo-root `Directory.Build.props`).\n- **DOTNET_HOST_PATH**: `RunnerUtilities.GetMSBuildEnvironmentVariables` (in `src/UnitTests.Shared/RunnerUtilities.cs`) sets `DOTNET_HOST_PATH` to the bootstrap dotnet when tests launch MSBuild as a child process, so tasks like `RoslynCodeTaskFactory` resolve the right host. Don't override `DOTNET_HOST_PATH` from a test.\n\n## Flags — MTP vs VSTest pitfalls\n\nThis repo uses MTP with the xUnit v3 MTP runner, **not** VSTest. Many familiar `dotnet test` flags silently do nothing. Key differences:\n\n- **Use** `--report-trx` (not `--logger trx`), `--coverage` (not `--collect \"XPlat Code Coverage\"`), `--filter-method`/`--filter-class`/`--filter-trait` for xUnit v3 native filtering.\n- **Don't use** `--filter`, `--nologo`, `--blame`, `--settings *.runsettings`, `--diag`, `--collect`, or `-- RunConfiguration.MaxCpuCount=...` — these are VSTest-only and ignored.\n- **Still works**: `-c`, `-f`, `--no-restore`, `--no-build`, `-v`, `-bl:`, `-p:Property=Value` — these are interpreted by `dotnet test` itself before MTP sees them.\n\nFor comprehensive MTP vs VSTest flag reference, see the `run-tests` skill from the `dotnet-test` plugin.\n\n## Dev loop: fast and scoped\n\nAim for sub-30s iterations.\n\n> Examples below use Windows-style backslashes and PowerShell line continuations. Forward slashes work everywhere with `dotnet` (`src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj`); on Linux/macOS use `/` and shell line continuations (`\\`), and the exe path becomes `artifacts/bin/<Proj>/Debug/net11.0/<Proj>` (no `.exe`).\n\n1. **Run via `dotnet test` with a single TFM and filter** (incremental build handles rebuilding automatically):\n   ```powershell\n   dotnet test src\\Tasks.UnitTests\\Microsoft.Build.Tasks.UnitTests.csproj `\n     -f net11.0 -- --filter-method \"*MyFeature*\"\n   ```\n2. **Or run the test exe directly** (fastest — no SDK overhead; build first if source changed):\n   ```powershell\n   dotnet build src\\Tasks.UnitTests\\Microsoft.Build.Tasks.UnitTests.csproj -c Debug -f net11.0\n   artifacts\\bin\\Microsoft.Build.Tasks.UnitTests\\Debug\\net11.0\\Microsoft.Build.Tasks.UnitTests.exe `\n     --filter-method \"*MyFeature*\" --no-progress\n   ```\n   The exe path is TFM-specific: `Debug\\net11.0\\...exe` for net11.0, `Debug\\net472\\...exe` for net472. Switching TFM without rebuilding silently runs stale binaries.\n\n### Speeding up the dev loop further\n\nThese trade safety for speed — use during iteration, **revert before final validation**:\n\n-- **Single TFM**: pass `-f net11.0`. Halves runtime on Windows by skipping `net472`.\n- **Temporarily relax single-threaded execution**: drop a `xunit.runner.json` next to the test project (or override the existing one) with:\n  ```json\n  {\n    \"$schema\": \"https://xunit.net/schema/current/xunit.runner.schema.json\",\n    \"longRunningTestSeconds\": 60,\n    \"maxParallelThreads\": -1,\n    \"parallelizeTestCollections\": true\n  }\n  ```\n  Or pass them as runner args after `--`: `-- xUnit.MaxParallelThreads=-1 xUnit.ParallelizeTestCollections=true`. Expect flakes in tests that touch env vars, cwd, the file system, or the global ProjectCollection — **don't ship a fix that depends on this being on**.\n- **Skip bootstrap packaging** for non-bootstrap test projects:\n  `dotnet build ... -p:CreateBootstrap=false` (useful when the local bootstrap SDK payload is missing).\n- **Narrow with traits**: `--filter-trait Category=mytraitduringdev` if you've tagged a focused subset.\n- **Skip code coverage on Linux/macOS**: run the test exe directly without `--coverage`. This repo does not expose a supported `dotnet test` property switch to disable the auto-added coverage arguments.\n\n## Final validation pass\n\nBefore saying \"done,\" run the heavy configuration. Don't skip TFMs and don't keep parallel-overrides.\n\n1. **Restore parallelism settings** (revert any local `xunit.runner.json` change).\n2. **Run all TFMs for affected projects**:\n   ```powershell\n   dotnet test src\\Build.UnitTests\\Microsoft.Build.Engine.UnitTests.csproj -c Release\n   dotnet test src\\Tasks.UnitTests\\Microsoft.Build.Tasks.UnitTests.csproj -c Release\n   # Add other UnitTests projects whose code paths you touched\n   ```\n3. **For broad changes (engine, framework, shared)**, run the full repo test suite (~9 minutes — do not cancel):\n   ```powershell\n   .\\build.cmd -test -c Release       # Windows\n   ./build.sh --test -c Release       # Linux/macOS\n   ```\n4. **Capture a TRX** if reporting results to the user or CI:\n   ```powershell\n   dotnet test <project> -c Release `\n     --report-trx --report-trx-filename validation.trx `\n     --results-directory artifacts\\TestResults\n   ```\n\n## Reading the summary line\n\nMTP's final summary reports `passed`, `failed`, and `skipped` separately. **Always check the `skipped` count** — platform-conditional attributes (`WindowsOnlyFact`, `UnixOnlyFact`, etc.) and the auto trait filters cause expected skips, but a sudden jump in skipped count can hide a regression where a test became inapplicable on the current platform without you intending it.\n\n## Picking the right project to run\n\nMatch the source area you changed to its `*.UnitTests` project:\n\n| Source area | Test project |\n|-------------|--------------|\n| `src/Build/**` (engine, evaluation, backend) | `src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj` |\n| `src/Framework/**` | `src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj` |\n| `src/Tasks/**` | `src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj` |\n| `src/Utilities/**` | `src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj` |\n| `src/MSBuild/**` (CLI) | `src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj` |\n| `src/Build/BuildCheck/**` | `src/BuildCheck.UnitTests/Microsoft.Build.BuildCheck.UnitTests.csproj` |\n| `src/Shared/**` | Run the consumers above (Build, Tasks, Utilities) — shared code is linked into all of them. |\n\n## Quick reference\n\n| Scenario | Command |\n|----------|---------|\n| Fast scoped dev loop | `dotnet test <proj> -f net11.0 -- --filter-method \"*X*\"` |\n| Direct test exe | `artifacts\\bin\\<Proj>\\Debug\\net11.0\\<Proj>.exe --filter-method \"*X*\" --no-progress` |\n| Single test by name | `dotnet test <proj> -- --filter-method \"*MyTestMethod*\"` |\n| Final per-project validation | `dotnet test <proj> -c Release` (all TFMs) |\n| Final full validation | `.\\build.cmd -test -c Release` / `./build.sh --test -c Release` |\n| TRX report | `--report-trx --report-trx-filename out.trx --results-directory artifacts\\TestResults` |\n\n## See also\n\n- `.github/instructions/tests.instructions.md` — test authoring conventions (xUnit v3, Shouldly, `TestEnvironment`, `MockLogger`).\n- `src/Shared/UnitTests/xunit.runner.json` — repo-wide xUnit settings.\n- `src/Directory.Build.targets` — `XunitOptions`, auto trait filters, coverage wiring.\n- `documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md`\n",".github/skills/use-bootstrap-msbuild/SKILL.md":"---\nname: use-bootstrap-msbuild\ndescription: Guide for testing bug reproductions against locally-built MSBuild. Use this when you have a repro project and want to verify a fix works before submitting a PR.\n---\n\n# Testing Bug Reproductions with Bootstrap MSBuild\n\nThis skill guides you through testing a bug reproduction project against your locally-built MSBuild to verify a fix.\n\n## Overview\n\nAfter making changes to MSBuild, you need to test them against a repro project. The \"bootstrap\" is a self-contained MSBuild installation built from your local changes. It includes all dependencies needed to build real projects.\n\n## Step 1: Build MSBuild with Bootstrap\n\nBuild MSBuild to create the bootstrap directory:\n\n```powershell\n# Windows\n.\\build.cmd\n\n# Unix/macOS\n./build.sh\n```\n\nThis creates the bootstrap at `artifacts\\bin\\bootstrap\\` with your changes.\n\nRerun this after making any code change. If run in the default mode, there may be some errors on subsequent builds about locked files (due to MSBuild worker node processes lingering). If so, run `./artifacts/bin/bootstrap/core/dotnet build-server shutdown`.\n\n## Step 2: Run Your Repro Project\n\n### .NET Core / .NET SDK Projects\n\nUse the bootstrap `dotnet` CLI directly (preferred):\n\n```powershell\n# Windows\nartifacts\\bin\\bootstrap\\core\\dotnet.exe build <path-to-repro.csproj>\n```\n\n```bash\n# Unix/macOS\n./artifacts/bin/bootstrap/core/dotnet build <path-to-repro.csproj>\n```\n\nAll of the usual command line arguments should work, including `-bl` to create binlogs.\n\n### .NET Framework Projects\n\nIf the problem is specific to the .NET Framework `MSBuild.exe` that is used in Visual Studio, and you're running on Windows, you can use the bootstrap MSBuild.exe directly:\n\n```powershell\nartifacts\\bin\\bootstrap\\net472\\MSBuild\\Current\\Bin\\MSBuild.exe <path-to-repro.csproj>\n```\n\n**Note**: The .NET Framework bootstrap output will only be complete when built on Windows using `MSBuild.exe`.\n\n### Changes not reflected\n\n1. Verify bootstrap was rebuilt: check `artifacts\\bin\\bootstrap\\core\\sdk\\*\\MSBuild.dll` timestamp\n2. Kill any lingering MSBuild server processes:\n   ```powershell\n   ./artifacts/bin/bootstrap/core/dotnet build-server shutdown\n   # Or use the helper function after sourcing msbuild-build-env.ps1\n   killdotnet\n   ```\n\n### Repro works with bootstrap but not with installed MSBuild\n\nYour fix is working! The repro uses your local changes while the installed MSBuild has the bug.\n\n## Quick Reference\n\n| Scenario | Command |\n|----------|---------|\n| Build bootstrap | `.\\build.cmd` |\n| .NET Core repro | `artifacts\\bin\\bootstrap\\core\\dotnet.exe build <project>` |\n| .NET Framework repro | `artifacts\\bin\\bootstrap\\net472\\MSBuild\\Current\\Bin\\MSBuild.exe <project>` |\n\n## See Also\n\n- [Bootstrap Documentation](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Bootstrap.md)\n- [Building and Debugging Guide](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md)\n","AGENTS.md":"# Agent Instructions\n\nInstructions for GitHub Copilot and other AI coding agents working with the MSBuild repository.\n\n## Repository Overview\n\n**MSBuild** is the Microsoft Build Engine - performance-critical infrastructure for .NET and Visual Studio builds. This repository contains the code for the MSBuild build engine, including its public C# API, its internal implementation of the MSBuild programming language, and core targets and tasks used for builds.\n\n### Key Components\n- **Microsoft.Build**: Core MSBuild engine and public API\n- **Microsoft.Build.Framework**: Framework interfaces and base types\n- **Microsoft.Build.Tasks**: Built-in MSBuild tasks\n- **Microsoft.Build.Utilities**: Utility classes for task authors\n- **MSBuild CLI**: Command-line tool for invoking builds\n\n### Technology Stack\n- .NET 10.0 and .NET Framework 4.7.2\n- C# 14 features (especially collection expressions)\n- xUnit with Shouldly for testing\n- Multi-platform support (Windows, Linux, macOS)\n\n## General\n\n* Performance is the top priority - minimize allocations, avoid LINQ in hot paths, use efficient algorithms.\n* Always use the latest C# features, currently C# 14, especially collection expressions (`[]` over `new Type[]`).\n* Match the style of surrounding code when making edits, but modernize aggressively for substantial changes.\n\n## Code Review Instructions\n\nOfficial builds treat all warnings as errors, so do not introduce new warnings.\n\n### Performance Considerations\n\nWhen reviewing pull requests:\n\n* **Flag any unnecessary allocations** in hot paths\n* **Flag LINQ usage** in performance-critical code paths\n* **Check for proper use of `Span<T>`** and `ReadOnlySpan<T>` for string parsing\n* **Ensure immutable collections** use the correct type (`ImmutableArray<T>` and `FrozenDictionary<TKey, TValue>` for read-heavy, `ImmutableList<T>` for incremental building)\n\n### NuGet Feed Configuration\n\nWhen reviewing pull requests:\n\n* **Flag any changes to NuGet.config** that add external package sources without justification\n* Package sources should use approved internal feeds when possible\n\n## Formatting\n\n* Apply code-formatting style defined in `.editorconfig`.\n* Prefer file-scoped namespace declarations and single-line using directives.\n* Insert a newline before the opening curly brace of any code block.\n* Use pattern matching and switch expressions wherever possible.\n* Use `nameof` instead of string literals when referring to member names.\n\n### Nullable Reference Types\n\n* **New files**: Always use nullable reference types (do NOT add `#nullable disable`)\n* **Existing files with `#nullable disable`**: Match the existing style; don't add nullable annotations (`?`) to types\n* **Existing files with nullable enabled**: Use proper nullable annotations\n* Always use `is null` or `is not null` instead of `== null` or `!= null`\n\n## Performance Best Practices\n\n### Range Pattern Matching\n\n```csharp\n// GOOD: Clear and efficient\nreturn errorNumber switch\n{\n    >= 3001 and <= 3999 => Category.Tasks,\n    >= 4001 and <= 4099 => Category.General,\n    >= 4100 and <= 4199 => Category.Evaluation,\n    _ => Category.Other\n};\n```\n\n### String Handling\n\n* Use `MSBuildNameIgnoreCaseComparer` for case-insensitive comparisons of MSBuild names; use `StringComparer.OrdinalIgnoreCase` only for non-MSBuild string comparisons\n* Use `char.ToUpperInvariant()` for single-character comparisons\n* Use `ReadOnlySpan<char>` and `Slice()` to avoid string allocations\n* Use `int.TryParse(span, out var result)` on .NET Core+ for allocation-free parsing\n\n### Inlining Hot Paths\n\n```csharp\n[MethodImpl(MethodImplOptions.AggressiveInlining)]\nprivate static bool IsCompilerPrefix(string value) => ...\n```\n\n### Immutable Collections\n\n**Build once, read many times** (most common in MSBuild):\n```csharp\nImmutableArray<string> items = source.Select(x => x.Name).ToImmutableArray();\nFrozenDictionary<string, int> lookup = pairs.ToFrozenDictionary(x => x.Key, x => x.Value);\n```\n\n**Build incrementally over time**:\n```csharp\nImmutableList<string> items = ...;  // Use when adding items one by one\nImmutableDictionary<string, int> lookup = ...;\n```\n\n## Building\n\nMost development can use `dotnet build`. For the most complete possible build, use `MSBuild.exe` from Visual Studio 2026. See the [Windows build prerequisites](README.md#building-msbuild-with-visual-studio-2026-on-windows).\n\nNEVER pipe MSBuild output to a file. If you want a list of errors, add `-flp:\"v=q;LogFile=ErrorsAndWarnings.log\"` to the arguments.\n\nWhen considering a subset of build errors, always look at the BEGINNING of the set, not the end.\n\n## Individual projects\n\nBuild individual projects with `dotnet msbuild {path/to/project.csproj} -v:q`.\n\n### Whole-repo Build Commands\n\n| Platform | Command | Timeout |\n|----------|---------|---------|\n| Windows | `.\\build.cmd -v quiet` | 300+ seconds (~2-3 minutes) |\n| macOS/Linux | `./build.sh -v quiet` | 300+ seconds (~2-3 minutes) |\n\n### Bootstrap Environment Setup\n\nAfter building the whole repo, to use the just-built MSBuild to build things use the bootstrap environment (described in [`skills/use-bootstrap-msbuild](.github/skills/use-bootstrap-msbuild/SKILL.md)).\n\n### Build Troubleshooting\n\n* If build fails with \"Could not resolve SDK\", run the bootstrap environment script\n* Verify `dotnet --version` shows the preview/internal version\n* Use repository sample projects for testing, not external projects\n* Build artifacts go to `./artifacts/` directory\n\n### Running Tests\n\n**Windows:**\n```cmd\n# Full test suite (~9 minutes) - NEVER CANCEL\n.\\build.cmd -test\n\n# Individual test project (recommended):\ndotnet test src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj\n```\n\n**macOS/Linux:**\n```bash\n# Full test suite (~9 minutes) - NEVER CANCEL\n./build.sh --test\n\n# Individual test project (recommended):\ndotnet test src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj\n```\n\nYou can run a single unit test with `-- --filter-method \"*{methodname}*\"`, for example `dotnet test src/Framework.UnitTests/ -- --filter-method \"*ExerciseBuildEventContext*\"`.\n\n### Test Verification\n\n* **Individual Test Project**: ~10-60 seconds per project\n* **Full Test Suite**: ~9 minutes\n\n## Project Layout and Architecture\n\n### Directory Structure\n\n```\nsrc/\n├── Build/                    # Core MSBuild engine (Microsoft.Build)\n├── Build.UnitTests/          # Unit tests for core engine\n├── MSBuild/                  # MSBuild command-line tool\n├── Framework/                # MSBuild Framework (Microsoft.Build.Framework)\n├── Framework.UnitTests/      # Unit tests for framework\n├── Tasks/                    # Built-in MSBuild tasks (Microsoft.Build.Tasks)\n├── Tasks.UnitTests/          # Unit tests for tasks\n├── Utilities/                # MSBuild utilities (Microsoft.Build.Utilities)\n├── Utilities.UnitTests/      # Unit tests for utilities\n├── Shared/                   # Shared code across assemblies\n└── Samples/                  # Sample projects and extensions\n\nartifacts/\n├── bin/                      # Built binaries\n│   └── bootstrap/\n│       └── core/\n│           └── MSBuild.dll   # Built MSBuild executable\n├── sdk-build-env.sh          # Bootstrap script (Linux/macOS)\n├── msbuild-build-env.bat     # Bootstrap script (Windows)\n└── packages/                 # Built NuGet packages\n\ndocumentation/\n├── wiki/                     # Developer documentation\n├── specs/                    # Technical specifications\n└── *.md                      # Various documentation files\n```\n\n### Key Configuration Files\n\n* **`global.json`**: Pins .NET SDK version - never modify without explicit request\n* **`NuGet.config`**: Package source configuration - never modify without explicit request\n* **`.editorconfig`**: Code formatting rules\n* **`Directory.Build.props`**: Shared MSBuild properties across all projects\n* **`Directory.Packages.props`**: Centralized package version management\n* **`MSBuild.slnx`**: Main solution file\n\n## Validation Checklist\n\nBefore completing any change:\n\n1. ✅ Full build completes successfully (`.\\build.cmd` or `./build.sh`)\n2. ✅ Bootstrap environment activates correctly (`dotnet --version` shows preview)\n3. ✅ Sample project builds: `dotnet build src/Samples/Dependency/Dependency.csproj`\n4. ✅ Relevant unit tests pass\n5. ✅ `dotnet artifacts/bin/bootstrap/core/MSBuild.dll --help` works\n\n## Do NOT Modify\n\n* `artifacts/` directory contents - Generated during build\n* `.dotnet/` directory contents - Local SDK location\n\nSee **Key Configuration Files** section for files that should not be modified without explicit request.\n\n## Documentation\n\nWhen making changes, check if related documentation exists in the `documentation/` folder (including `documentation/specs/`) and update it to reflect your changes. Keep documentation in sync with code changes.\n\n## Breaking Changes\n\nBecause MSBuild is a critical part of the build process for a huge number of customers, we avoid breaking changes. Adding new errors or warnings, even when well-intentioned and pointing out things that are very likely to be wrong, is an unacceptable breaking change. Adding warnings is a breaking change because many production builds use `/WarnAsError`.\n\nThe exception to this policy is in new, opt-in behavior. In new, opt-in functionality, liberally emit warnings and errors--they can always be removed later.\n\nWhen reviewing PRs, always consider whether the behavior change could be experienced as a break in existing builds and flag any new warnings or errors.\n\n## Development Workflow\n\n1. Make your changes to source code\n2. Run the full build (WAIT for completion - takes 2-3 minutes):\n   - Windows: `.\\build.cmd -v quiet`\n   - macOS/Linux: `./build.sh -v quiet`\n4. Test your changes end-to-end using the bootstrap output: `dotnet build src/Samples/Dependency/Dependency.csproj`\n5. Run relevant individual tests, not the full test suite\n6. Commit your changes\n\n## Trust These Instructions\n\nThese instructions are comprehensive and tested. Only search for additional information if:\n1. The instructions appear outdated or incorrect\n2. You encounter specific errors not covered here\n3. You need details about new features not yet documented\n\nFor most development tasks, following these instructions should be sufficient to build, test, and validate changes successfully.\n\n## Updating these instructions\n\nWhen working on a task, if user input is required to complete the task or feedback is provided for guidance around a specific area of the code, evaluate that feedback/guidance and update this document to incorporate that feedback if it's missing.  This document should be a live, evolving set of instructions.\n","plugins/mt-migration/README.md":"# mt-migration plugin\n\nA GitHub Copilot plugin that bundles the MSBuild **multithreaded task migration** playbook plus an MT-specific PR reviewer.\n\nUse this on any repository that authors MSBuild tasks consuming the\n`IMultiThreadableTask` / `TaskEnvironment` / `[MSBuildMultiThreadableTask]` API\nsurface from `Microsoft.Build.Framework` — currently `dotnet/msbuild`,\n`dotnet/sdk`, and downstream task assemblies (NuGet, EF, ASP.NET Core, etc.).\n\n## Contents\n\n```\nmt-migration/\n├── plugin.json\n├── skills/\n│   └── multithreaded-task-migration/\n│       └── SKILL.md         # The 8 deadly sins, ToolTask hazards, helper patterns, test patterns, sign-off checklist\n└── agents/\n    └── mt-migration-reviewer.agent.md   # MT-specific PR reviewer that follows call chains end-to-end\n```\n\n## What it does\n\n| Component | When invoked | What it does |\n|---|---|---|\n| `multithreaded-task-migration` skill | Author migrating a task | Step-by-step migration recipe + compatibility red-team checklist. Distilled from ~50 merged migration PRs across `dotnet/msbuild` and `dotnet/sdk`. |\n| `mt-migration-reviewer` agent | Reviewing an MT migration PR | Delegates the 24-dimension general review to the host repo's expert reviewer (if any), then layers MT-specific findings on top. Mandatorily traces every call chain from `Execute()` to leaves and reports hazards by chain, not by file. Verifies tests are not theater. |\n\nThe reviewer is deliberately complementary to a generic code reviewer: it does\nnot re-explain migration steps, does not redo style/perf/naming review, and\nwill not flag a clean attribute-only migration as needing a concurrency test if\nthe call-chain audit comes back clean.\n\n## Installing\n\n### As a local plugin (from a clone of this repo)\n\n```sh\n# from a GitHub Copilot session\n/plugin install file://$(pwd)/plugins/mt-migration\n```\n\n### Across multiple repos (recommended)\n\nPublish this plugin folder to a small standalone repo (e.g.\n`your-org/mt-migration-plugin`) and install from there. The plugin contains no\nhard-coded references to `dotnet/msbuild` repo-local filesystem paths — only to\nthe public `Microsoft.Build.Framework` API surface (with source links for\nreference) — so it works on any task-authoring repo unchanged.\n\n```sh\n/plugin install github:your-org/mt-migration-plugin\n```\n\n## Relationship to the in-repo skill\n\nThis plugin is the **single, canonical** home of the MT migration skill. It is not duplicated under `.github/skills/` in `dotnet/msbuild` — install the plugin to use it locally. The skill-validator workflow in `dotnet/msbuild` only scans `.github/skills/` and `.github/agents/`, so the plugin lives outside that scope and is not subject to it.\n\n## Updating the skill from PR experience\n\nThe skill's \"deadly sins\" and call-chain hazard table grow as new\ndefect classes are found in merged migrations. To contribute:\n\n1. Find a defect that was caught in PR review and would have been missed by\n   the existing skill.\n2. Add a sin / hazard / pattern with a real-world citation (PR + line range).\n3. Update `skills/multithreaded-task-migration/SKILL.md`.\n4. Keep the agent file slim — it should keep delegating to the skill rather\n   than absorbing the rules itself.\n","plugins/mt-migration/plugin.json":"{\n  \"name\": \"mt-migration\",\n  \"version\": \"0.1.0\",\n  \"description\": \"MSBuild multithreaded task migration toolkit: the migration playbook + an MT-specific code reviewer that traces call chains end-to-end. Portable across repos that consume Microsoft.Build.Framework's TaskEnvironment/IMultiThreadableTask API (e.g., dotnet/msbuild, dotnet/sdk, and any task assembly migrating to the MT model).\",\n  \"skills\": [\"./skills/\"],\n  \"agents\": [\n    \"./agents/mt-migration-reviewer.agent.md\"\n  ]\n}\n","plugins/mt-migration/skills/multithreaded-task-migration/SKILL.md":"---\nname: multithreaded-task-migration\ndescription: Guide for migrating MSBuild tasks to multithreaded mode support, including compatibility red-team review. Use this when converting tasks to thread-safe versions, implementing IMultiThreadableTask, adding TaskEnvironment support, or auditing migrations for behavioral compatibility.\n---\n\n# Migrating MSBuild Tasks to Multithreaded API\n\nMSBuild's multithreaded execution model requires tasks to avoid global process state (working directory, environment variables). Thread-safe tasks declare this capability via `MSBuildMultiThreadableTask` and use `TaskEnvironment` from `IMultiThreadableTask` for safe alternatives.\n\n## Migration Steps\n\n### Step 1: Update Task Class Declaration\n\na. Ensure the task implementing class is decorated with the `MSBuildMultiThreadableTask` attribute.\nb. Implement `IMultiThreadableTask` **only if** the task needs `TaskEnvironment` APIs (path absolutization, env vars, process start). If the task has no file/environment operations (e.g., a stub class), the attribute alone is sufficient.\n\n```csharp\n[MSBuildMultiThreadableTask]\npublic class MyTask : Task, IMultiThreadableTask\n{\n    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;\n    ...\n}\n```\n\n**Note**: `[MSBuildMultiThreadableTask]` has `Inherited = false` — it must be on each concrete class, not just the base. The corollary is easy to miss: the base class still executes multithreaded, but is not analyzed. Audit the whole base chain — see [Unsafe Code in an Unannotated Base Class](#unsafe-code-in-an-unannotated-base-class).\n\n### Step 2: Absolutize Paths Before File Operations\n\nAll path strings must be absolutized with `TaskEnvironment.GetAbsolutePath()` before use in file system APIs. This resolves paths relative to the project directory, not the process working directory.\n\n```csharp\nAbsolutePath absolutePath = TaskEnvironment.GetAbsolutePath(inputPath);\nif (File.Exists(absolutePath))\n{\n    string content = File.ReadAllText(absolutePath);\n}\n```\n\nThe [`AbsolutePath`](https://github.com/dotnet/msbuild/blob/main/src/Framework/PathHelpers/AbsolutePath.cs) struct:\n- `Value` — the absolute path string\n- `OriginalValue` — preserves the input path (use for error messages and `[Output]` properties)\n- Implicitly convertible to `string` for File/Directory API compatibility\n- `GetCanonicalForm()` — resolves `..` segments and normalizes separators (see [Sin 5](#sin-5-canonicalization-mismatch))\n\n**CAUTION**: `GetAbsolutePath()` throws `ArgumentException` for null/empty inputs. See [Sin 3](#sin-3-null-coalescing-that-changes-control-flow) and [Sin 6](#sin-6-exception-type-change) for compatibility implications.\n\n### Step 3: Replace Environment Variable APIs\n\n| BEFORE (UNSAFE)                                  | AFTER (SAFE)                                       |\n|--------------------------------------------------|----------------------------------------------------|\n| `Environment.GetEnvironmentVariable(\"VAR\");`       | `TaskEnvironment.GetEnvironmentVariable(\"VAR\");`     |\n| `Environment.SetEnvironmentVariable(\"VAR\", \"v\");`  | `TaskEnvironment.SetEnvironmentVariable(\"VAR\", \"v\");` |\n\n### Step 4: Replace Process Start APIs\n\n| BEFORE (UNSAFE - inherits process state)           | AFTER (SAFE - uses task's isolated environment)     |\n|----------------------------------------------------|-----------------------------------------------------|\n| `var psi = new ProcessStartInfo(\"tool.exe\");`      | `var psi = TaskEnvironment.GetProcessStartInfo();`  |\n|                                                    | `psi.FileName = GetFullPathToTool(); // must be absolute` |\n\n## Updating Unit Tests\n\nBuilt-in MSBuild tasks now initialize `TaskEnvironment` with a `MultiProcessTaskEnvironmentDriver`-backed default. Tests creating instances of built-in tasks no longer need manual `TaskEnvironment` setup. For custom or third-party tasks that implement `IMultiThreadableTask` without a default initializer, set `TaskEnvironment = TaskEnvironment.Fallback` (or use `TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(path)` to point at a specific project directory).\n\n## APIs to Avoid\n\n| Category | APIs | Alternative |\n|---|---|---|\n| **Forbidden** | `Environment.Exit`, `FailFast`, `Process.Kill`, `ThreadPool.SetMin/MaxThreads`, `Console.*` | Return false, throw, or use `Log` |\n| **Use TaskEnvironment** | `Environment.CurrentDirectory`, `Get/SetEnvironmentVariable`, `Path.GetFullPath`, `ProcessStartInfo` | See Steps 2-4 |\n| **Need absolute paths** | `File.*`, `Directory.*`, `FileInfo`, `DirectoryInfo`, `FileStream`, `StreamReader/Writer` | Absolutize first (File System APIs) |\n| **Need absolute paths (analyzer-invisible)** | `AssemblyName.GetAssemblyName`, `XDocument/XElement/XmlDocument.Load(string)`, `XmlReader/XmlWriter.Create(string)`, `ZipFile.*`, `X509CertificateLoader`, `Image.FromFile`, `Assembly.LoadFrom` | Absolutize first — **the analyzer does not flag these** |\n| **Review required** | `Assembly.Load*`, `Activator.CreateInstance*` | Check for version conflicts |\n\n### Analyzer-Invisible Path Consumers\n\n`MSBuildTask0003` monitors a fixed list of types (`File`, `Directory`, `FileInfo`, `DirectoryInfo`, `FileStream`, `StreamReader`, `StreamWriter`, `FileSystemWatcher`). Any *other* API that accepts a path string and touches disk is equally unsafe but produces **no diagnostic**.\n\nIn a ~150-task migration across dotnet/arcade, dotnet/source-build-assets and the dotnet/dotnet VMR, `AssemblyName.GetAssemblyName` on a raw input was the single most-repeated defect. Do not treat a clean analyzer run as evidence that path handling is complete — see [Verification](#verification-a-clean-analyzer-run-is-not-a-migration).\n\nNote the overload distinction: the *string* overloads are hazards; `XDocument.Load(stream)` and `new StreamReader(stream)` are fine, because the caller already resolved the path to open the stream.\n\n## Practical Notes\n\n### CRITICAL: Trace All Path String Usage\n\nTrace every path string through all method calls and assignments to find all places it flows into file system operations — including helper methods that may internally use File System APIs.\n\n1. Find every path string (e.g., `item.ItemSpec`, function parameters)\n2. Trace downstream through all method calls\n3. Absolutize BEFORE any code path that touches the file system\n4. Use `OriginalValue` for user-facing output (logs, errors) — see [Sin 2](#sin-2-errorlog-message-path-inflation)\n\n**Trace through abstractions, not just concrete calls.** A path can reach the file system without a single `System.IO` type appearing in the task. In `InstallDotNetTool` (dotnet/arcade), the `DestinationPath`, `DotnetPath` and `WorkingDirectory` inputs flow raw through `IFileSystem` and `ICommandFactory` interfaces — the task body looks completely clean, and the analyzer reports nothing. When a task input is passed to an interface or delegate, resolve it at the task boundary rather than hoping the implementation does.\n\n**Resolve once, at the boundary.** The recurring failure is not \"forgot to absolutize\" but \"absolutized at three of four use sites\". If a variable is going to be used as a path at all, convert it to `AbsolutePath` where it enters the task and keep it that way — see [Sin 8](#sin-8-swallowed-exceptions-hiding-an-unresolved-path).\n\n### Exception Handling in Batch Operations\n\nIn batch processing (iterating over files), `GetAbsolutePath()` throwing on one bad path aborts the entire batch. Match the original task's error semantics:\n\n```csharp\nbool success = true;\nforeach (ITaskItem item in SourceFiles)\n{\n    try\n    {\n        AbsolutePath path = TaskEnvironment.GetAbsolutePath(item.ItemSpec);\n        ProcessFile(path);\n    }\n    catch (ArgumentException ex)\n    {\n        Log.LogError(\"Invalid path '{0}': {1}\", item.ItemSpec, ex.Message);\n        success = false;\n    }\n}\nreturn success;\n```\n\n### Prefer AbsolutePath Over String\n\nStay in the `AbsolutePath` world — it's implicitly convertible to `string` where needed. Avoid round-tripping through `string` and back.\n\n### TaskEnvironment is Not Thread-Safe\n\nIf your task spawns multiple threads internally, synchronize access to `TaskEnvironment`. Each task *instance* gets its own environment, so no synchronization between tasks is needed.\n\n### API Discipline When Plumbing Through Helpers\n\nWhen the migration ripples into shared helpers:\n\n- **Helper signatures: take `AbsolutePath`, not `(string path, string pathForMessages)`**. Two-string signatures drift apart; the caller passing one `AbsolutePath` keeps `.Value` and `.OriginalValue` in lockstep.\n- **Repo-wide helpers belong in an extensions class** (e.g., `TaskEnvironmentExtensions`), not on the task. If a per-task helper looks generic, extract it.\n- **Don't condition behavior on MT-mode**. `TaskEnvironment.Fallback` already gives single-process semantics; `if (mtMode) { … } else { … }` doubles the maintenance surface and skips test coverage of one branch.\n- **Don't silently swallow `ArgumentException` from `GetAbsolutePath`** in a helper — log a diagnostic so customers can debug bad inputs.\n\n## References\n\n- [Thread-Safe Tasks Spec](https://github.com/dotnet/msbuild/blob/main/documentation/specs/multithreading/thread-safe-tasks.md)\n- [`AbsolutePath`](https://github.com/dotnet/msbuild/blob/main/src/Framework/PathHelpers/AbsolutePath.cs)\n- [`TaskEnvironment`](https://github.com/dotnet/msbuild/blob/main/src/Framework/TaskEnvironment.cs)\n- [`IMultiThreadableTask`](https://github.com/dotnet/msbuild/blob/main/src/Framework/IMultiThreadableTask.cs)\n\n---\n\n# Compatibility Red-Team Playbook\n\nAfter migration, review for behavioral compatibility. **Every observable difference is a bug until proven otherwise.**\n\nObservable behavior = `Execute()` return value, `[Output]` property values, error/warning message content, exception types, files written, and which code path runs.\n\n## The 8 Deadly Compatibility Sins\n\nReal bugs found during MSBuild task migrations. Every one shipped in initial \"passing\" code with green tests.\n\n**Edge-case discipline:** For every migrated code path, verify behavior when inputs are `null`, empty string (`\"\"`), or whitespace-only. `GetAbsolutePath` throws `ArgumentException` on null/empty — if the pre-migration code handled these differently (e.g., returned early, used a default, or threw a different exception type), the migration must preserve that behavior. Even if a scenario seems unlikely, treat it as a relevant finding if it is theoretically possible.\n\n### Sin 1: Output Property Contamination\n\nAbsolutized values leak into `[Output]` properties that users/other tasks consume.\n\n```csharp\n// BROKEN: ManifestPath was \"bin\\Release\\app.manifest\", now \"C:\\repo\\bin\\Release\\app.manifest\"\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(Path.Combine(OutputDirectory, name));\nManifestPath = abs; // implicit string conversion!\n\n// CORRECT: separate original form from absolutized path\nstring originalPath = Path.Combine(OutputDirectory, name);\nAbsolutePath outputPath = TaskEnvironment.GetAbsolutePath(originalPath);\nManifestPath = originalPath;          // [Output]: original form\ndocument.Save((string)outputPath);    // file I/O: absolute path\n```\n\n**Detect**: For every `[Output]` property, trace backward — is it ever assigned from an `AbsolutePath`?\n\n### Sin 2: Error/Log Message Path Inflation\n\nError messages and exception messages show absolutized paths instead of the user's original input.\n\n**Direct leakage** — passing an `AbsolutePath` to logging APIs:\n\n```csharp\n// BROKEN: \"Cannot find 'C:\\repo\\app.manifest'\" instead of \"Cannot find 'app.manifest'\"\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(path);\nLog.LogError(\"Cannot find '{0}'\", abs); // implicit conversion!\n\n// CORRECT: use OriginalValue\nLog.LogError(\"Cannot find '{0}'\", abs.OriginalValue);\n```\n\n**Indirect leakage** — exception messages from helpers that received the absolutized path:\n\n```csharp\n// BROKEN: ex.FileName / ex.Message embed the absolutized path\ncatch (FileNotFoundException ex) { Log.LogError(\"Not found: {0}\", ex.FileName); }\ncatch (Exception ex)              { Log.LogError(ex.Message); }\n\n// CORRECT: prefer the original input; if you must use the exception, sanitize\ncatch (FileNotFoundException ex) { Log.LogError(\"Not found: {0}\", abs.OriginalValue); }\ncatch (Exception ex)              { Log.LogError(ex.Message.Replace(abs.Value, abs.OriginalValue)); }\n```\n\n**Detect**: Search every `Log.LogError`/`LogWarning`/`LogMessage` — is any argument an `AbsolutePath`? Also check every `Log.LogError(ex.Message …)` / `ex.FileName` downstream of a `GetAbsolutePath` — did the exception originate from a helper that received the absolutized path?\n\n### Sin 3: Null Coalescing That Changes Control Flow\n\nAdding `?? \"\"` silently swallows an exception the old code relied on for error handling.\n\n```csharp\n// BEFORE: Path.GetDirectoryName(\"C:\\\") → null → Path.Combine(null, x) → ArgumentNullException\n//   → task fails with an exception / error logged → Execute() returns false\n\n// BROKEN: ?? \"\" added \"for safety\"\nstring dir = Path.GetDirectoryName(fileName) ?? string.Empty;\n// Path.Combine(\"\", x) succeeds silently → no error → Execute() returns TRUE!\n```\n\n**Detect**: For every `??` you added, ask: \"What happened when this was null before?\" If it threw and was caught → your `??` is a bug.\n\n### Sin 4: Try-Catch Scope Mismatch\n\n`GetAbsolutePath()` inside a try block leaves the absolutized value out of scope in the catch block. Helper methods in the catch (like `LockCheck`) then use the original non-absolute path.\n\n```csharp\n// CORRECT: hoist above try so catch can use it too\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(OutputManifest.ItemSpec);\ntry {\n    WriteFile(abs);\n} catch (Exception ex) {\n    string lockMsg = LockCheck.GetLockedFileMessage(abs);        // absolute → correct file\n    Log.LogError(\"Failed: {0}\", OutputManifest.ItemSpec, ...);   // original → user-friendly\n}\n```\n\n**Detect**: For every `GetAbsolutePath` inside a try, check if the catch block needs the absolutized value.\n\n### Sin 5: Canonicalization Mismatch\n\n`GetAbsolutePath` does NOT canonicalize. `Path.GetFullPath` does TWO things: absolutize AND canonicalize (`..` resolution, separator normalization). If the old code used `Path.GetFullPath` for dictionary keys, comparisons, or display, you must add `.GetCanonicalForm()`:\n\n```csharp\n// GetAbsolutePath(\"foo/../bar\")  → \"C:\\repo\\foo/../bar\"  (NOT canonical)\n// Path.GetFullPath(\"foo/../bar\") → \"C:\\repo\\bar\"         (canonical)\n\n// BROKEN for dictionary keys — \"C:\\repo\\foo\\..\\bar\" ≠ \"C:\\repo\\bar\"\nvar map = items.ToDictionary(p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec), ...);\n\n// CORRECT\nvar map = items.ToDictionary(\n    p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec).GetCanonicalForm(),\n    StringComparer.OrdinalIgnoreCase);\n```\n\n**Detect**: Find every `Dictionary`/`HashSet`/`ToDictionary` using path keys, and every place the old code called `Path.GetFullPath`. If canonicalization mattered, add `.GetCanonicalForm()`.\n\n### Sin 6: Exception Type Change\n\nOld code threw `FileNotFoundException` for missing files; new code throws `ArgumentException` from `GetAbsolutePath(\"\")` before reaching the file check. Custom catch blocks filtering by exception type may be bypassed. (`ExceptionHandling.IsIoRelatedException` catches `ArgumentException`, but task-specific handlers might not.)\n\n**Detect**: For every `GetAbsolutePath`, check what the old code threw for null/empty and whether the calling code has type-specific catch blocks.\n\n### Sin 7: `Path.IsPathRooted` as an Absolutize Gate\n\n`Path.IsPathRooted` returns `true` for **drive-relative** (`C:foo\\bar`) and **root-relative** (`\\foo\\bar`) Windows paths — both still depend on process current-directory / current-drive state. \"Absolutize only if not rooted\" silently leaves those paths process-dependent.\n\n`GetAbsolutePath` correctly handles all path forms — including the Windows edge cases (`C:foo`, `\\foo`) that `Path.IsPathRooted` considers \"rooted\" but that are still CWD/drive-dependent. Call it unconditionally; remove any `IsPathRooted` short-circuit.\n\n### Sin 8: Swallowed Exceptions Hiding an Unresolved Path\n\nThe usual mental model is \"unresolved path → `FileNotFoundException` → loud failure\". That model is wrong wherever a `catch` treats an exception as a *semantic answer*. There, an unresolved path does not fail the build — it silently returns the wrong answer.\n\nReal example from the dotnet/dotnet VMR (`CheckForPoison`), where the same variable is resolved three lines later:\n\n```csharp\n// BROKEN\ntry\n{\n    AssemblyName asm = AssemblyName.GetAssemblyName(fileToCheck);   // raw, relative-capable\n    ...\n    if (IsAssemblyFromSbrp(TaskEnvironment.GetAbsolutePath(fileToCheck))) { ... }   // resolved\n    else if (IsAssemblyPoisoned(TaskEnvironment.GetAbsolutePath(fileToCheck))) { ... }\n}\ncatch (BadImageFormatException) { /* not a managed assembly - fine */ }\n```\n\nThe `GetAssemblyName` call is only an \"is this a managed assembly?\" probe, and the catch means \"not an assembly\". So an unresolved path throws, gets swallowed, and **both poison checks are skipped** — turning a leaked binary into a false negative in the leak-detection gate itself. The build stays green while the check it exists to perform quietly stops working.\n\nThis sin is especially dangerous because it defeats the usual verification strategy: the decoy-CWD test (Pattern A) still \"passes\" unless it asserts on the *result*, not just on `Execute()` returning true.\n\n**Detect**: For every `catch` in a migrated task, ask what the handler *means*. If it means anything other than \"fail\" — \"not an assembly\", \"not found, use default\", \"unsupported, skip\" — then every path-consuming call inside the corresponding `try` must be absolutized, and the catch should be narrowed to the exception types that genuinely represent that semantic answer (`BadImageFormatException` here, **not** `IOException`/`ArgumentException`).\n\n---\n\n## Call-Chain Hazards Beyond the Task\n\nThe 7 sins above are *local* to the task body. The other half of MT migration is auditing the **transitive call chain** — helpers and shared utility classes the task reaches into.\n\n### Helpers That Capture Process State\n\nHelpers reached from `Execute()` can quietly depend on process state in any of these ways:\n\n- A relative path falls through to `Directory.GetCurrentDirectory()` (often as a \"fallback\") or to `Path.GetFullPath(x)` without a base.\n- An env var is read directly via `Environment.GetEnvironmentVariable` (not `TaskEnvironment`).\n- A `static` field is seeded from process state on first use (`static string s_x = Directory.GetCurrentDirectory()`), permanently capturing the first caller's environment for every later caller.\n- A BCL API echoes its input path back through `ex.Message` / `ex.FileName` (Sin 2).\n- The helper takes a `string path` and returns a `string` — losing the `.OriginalValue` distinction, so callers either lie in messages or absolutize twice.\n\n**Resolution patterns:**\n\n1. Add an MT-aware overload that takes `TaskEnvironment` (or an `AbsolutePath` / explicit fallback path). Legacy overload delegates with `TaskEnvironment.Fallback`. Don't branch on \"MT mode on/off\".\n2. Promote the parameter type to `AbsolutePath` so `.Value` and `.OriginalValue` travel together.\n3. Replace process-state-seeded static caches with `ConcurrentDictionary<TKey, TValue>` keyed on the inputs that determine uniqueness — never on process state.\n4. For cross-repo helpers (foundation types used by many tasks), migrate the foundation in a dedicated PR before the tasks that consume it; the hydration step is usually where `Path.GetFullPath` is hiding.\n\n### Unsafe Code in an Unannotated Base Class\n\n`[MSBuildMultiThreadableTask]` is `Inherited = false`, so it goes on each concrete task. The consequence people miss is the mirror image: **the base class runs multithreaded too, but nothing marks it as such** — and with `msbuild_task_analyzer.scope = multithreadable_only` (the recommended setting for incremental migration) the analyzer does not look inside it at all.\n\nFrom dotnet/arcade: `CreateAkaMSLinks` and `DeleteAkaMSLinks` were both annotated, both analyzer-clean. Their shared base contained:\n\n```csharp\n// AkaMSLinksBase - NOT annotated, so never analyzed\nFile.ReadAllText(ClientCertificate)   // ClientCertificate is a task input property\n```\n\nA monitored API (`File`) on a raw task input — exactly what the analyzer exists to catch — invisible purely because of where it lived.\n\n**Resolution:** when you annotate a task, audit its full base chain up to `Task`/`ToolTask`. If the base holds shared input properties or path handling, have the **base** implement `IMultiThreadableTask` and do the resolution there, so every derived task inherits the fix:\n\n```csharp\npublic abstract class AkaMSLinksBase : Task, IMultiThreadableTask\n{\n    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;\n    // ... resolve ClientCertificate here, once, for all derived tasks\n}\n```\n\nNote that implementing `IMultiThreadableTask` on the base is safe and does not change routing — routing keys off the attribute, which stays on the concrete classes.\n\n### Engine-Owned Shared State: `RegisterTaskObject`\n\nThe guidance about `static` fields has a less obvious sibling: `IBuildEngine4.GetRegisteredTaskObject` / `RegisterTaskObject`. Under MT, in-process thread nodes share this engine-owned state. Tasks in different worker or TaskHost processes do not.\n\nUsing these APIs is a **review signal, not a warning**. Check object thread safety, key collisions, and whether correctness depends on two task invocations sharing a cache.\n\nThe get/register sequence is not atomic. Two tasks can both miss and create an object. Only one equal-key object is retained; `RegisterTaskObject` returns no result, and MSBuild does not dispose an unretained object. Whether that matters depends on what is cached:\n\n| Cached computation | Review result |\n|---|---|\n| Pure and deterministic for the key | Safe if the losing object needs no cleanup, or the task cleans it up |\n| Deduplication is the point, such as logging once per build | Unsafe because both tasks can perform the side effect |\n| A failure | Check that the key includes every input that affects the result; otherwise do not cache it |\n\nReal examples from dotnet/arcade: `LocateDotNet` was safe because its computation was pure. `SingleError` was not safe because its purpose was to emit exactly one error per build, so its annotation was removed.\n\n### Tasks Instantiated Directly by Other Tasks\n\n`[MSBuildMultiThreadableTask]` is only honored when the TaskFactory creates the task (i.e., a target invokes it as `<MyTask … />`). A task created via `new MyTask()` inside another task's body bypasses the factory and **never gets `TaskEnvironment` injected** — it silently defaults to `TaskEnvironment.Fallback` (= process CWD).\n\nMigrating a nested task in isolation is therefore meaningless. Either migrate the parent and have it explicitly propagate its `TaskEnvironment` to the nested instance before calling `Execute()`, or restructure so the nested task is a regular TaskFactory-created task.\n\n**Detect:** grep for `new …Task()` followed by `.Execute()` — every direct instantiation is a hole.\n\n### ToolTask Override Hot Spots\n\n`ToolTask` subclasses inherit virtual methods that run *before or after* `Execute()` and frequently touch the file system. Audit every override:\n\n| Override | Hazard | Migration |\n|---|---|---|\n| `GenerateFullPathToTool()` | Builds `ProcessStartInfo.FileName` — relative tool path → wrong tool launched | Absolutize the tool path before returning |\n| `SkipTaskExecution()` | Up-to-date check on input/output timestamps using relative paths | Absolutize both sides of the comparison |\n| `ValidateParameters()` | `File.Exists` on input parameters | Absolutize before probing |\n| `GenerateResponseFileCommands()` / `GenerateCommandLineCommands()` | Tempting to absolutize args | **Don't** — the child's `WorkingDirectory` is the project dir, so relative args resolve correctly; absolutizing inflates user-visible output and can leak into tool diagnostics or generated artifacts |\n| `GetWorkingDirectory()` | Default `null` → child inherits host CWD | Leave alone if you use `TaskEnvironment.GetProcessStartInfo()`; it already sets `WorkingDirectory` |\n\n**Key contract:** `ProcessStartInfo.FileName` is resolved by the OS **before** `WorkingDirectory` takes effect, so the executable path must be absolute. Tool arguments are interpreted by the child process in its working directory, so they should stay relative.\n\n### Engine-Owned Environment Variables Are Immutable in MT Mode\n\nEnv vars consumed by the engine *before* tasks run (e.g., `MSBUILD*`-prefixed flags and the framework/SDK discovery variables) are snapshotted into engine caches at startup. Tasks that mutate them later have no observable effect on the engine — but the mutation appears to succeed locally, masking bugs.\n\nIf a migrated task previously mutated such a variable to influence a downstream tool, re-architect to pass the value via `ProcessStartInfo.EnvironmentVariables` on the child process instead of mutating the parent's environment.\n\n---\n\n## Test Patterns for MT Migrations\n\nA migration test must **fail when the migration is undone**. Tests that pass identically against pre- and post-migration code are theater. The two patterns below are the ones that reliably exercise MT-specific behavior.\n\n### Pattern A: Decoy-CWD Test (for tasks that touch the file system)\n\nSet the process working directory to a **decoy** dir with no relevant files; set `TaskEnvironment.ProjectDirectory` to a different dir with the inputs/expected outputs. The task must read/write against the project directory, not the decoy.\n\n```csharp\nusing TestEnvironment env = TestEnvironment.Create();\nTransientTestFolder projectDir = env.CreateFolder();\nTransientTestFolder decoyCwd = env.CreateFolder();\nFile.WriteAllText(Path.Combine(projectDir.Path, \"input.txt\"), \"expected\");\n\nenv.SetCurrentDirectory(decoyCwd.Path); // auto-restored when env is disposed\n\nvar task = new MyTask\n{\n    TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir.Path),\n    Input = \"input.txt\", // relative\n};\ntask.Execute().ShouldBeTrue();\n```\n\n**CRITICAL:** `Directory.SetCurrentDirectory` is **process-global**. xUnit parallelizes tests within a class by default — a CWD-mutating test will flake unless pinned to a non-parallel collection (`[Collection]` + `[CollectionDefinition(DisableParallelization = true)]`). The `IDisposable` restore protects sequential safety but not concurrent siblings.\n\n### Pattern B: Cross-Instance Independence (for tasks doing relative-path resolution)\n\nTwo task instances, two `TaskEnvironment.ProjectDirectory` values, same relative input. Assert each task's output is rooted to its own project directory — proving resolution is per-instance, not shared.\n\n### When NOT to Write a \"Migration Test\"\n\nAttribute-only migrations (just `[MSBuildMultiThreadableTask]`, no `IMultiThreadableTask`) on tasks whose `Execute()` body contains no file/env/process/static-state interactions have nothing to test — a decoy-CWD test would pass whether the attribute is present or not. **Don't write the test.** Document the call-chain audit conclusion in the PR description instead.\n\n### Test Hygiene\n\n- Use `TestEnvironment` / `TransientTestFolder` for temp dirs; never `Path.GetTempPath() + Guid.NewGuid()` with manual `try/finally`.\n- Hard-code expected assertion values; don't recompute them from `Path.Combine(...)` in the assertion — when the test fails you want to know what was actually wrong.\n\n---\n\n## Verification: A Clean Analyzer Run Is Not a Migration\n\n`Microsoft.Build.TaskAuthoring.Analyzer` is the right first line of defense, and it should be enabled. It is not sufficient on its own, and it is worth being concrete about why so you know what work it leaves you.\n\n**The data.** In a migration of ~150 tasks across dotnet/arcade, dotnet/source-build-assets and the dotnet/dotnet VMR, the analyzer was enabled in all three repos and every build was **0 warnings, 0 errors**. A manual audit afterwards found **9 real defects**:\n\n| Why the analyzer missed it | Count | Example |\n|---|---|---|\n| The code was outside the analysis scope | 2 | unsafe call in an unannotated base class |\n| The API is not on the monitored list | 3 | `AssemblyName.GetAssemblyName` |\n| The failure class is not modeled at all | 4 | task-object race, memoized failure, nested task construction, path crossing a DI boundary |\n\nThe last row is the important one: those are dataflow and lifetime problems, not banned-API problems. No amount of allowlist tuning reaches them, which is why the [Red-Team Audit Protocol](#red-team-audit-protocol) below is not optional.\n\n**Why \"it worked when I tested it\" is weak evidence.** An unresolved relative path in MT mode does not reliably throw. It resolves against whatever the shared node's current directory happens to be, which depends on which project scheduled first. So the same defect passes locally single-threaded every time, passes in CI most of the time, and fails occasionally on one machine under load. And per [Sin 8](#sin-8-swallowed-exceptions-hiding-an-unresolved-path), some never throw at all.\n\n**Verification checklist.** The [Sign-Off Checklist](#sign-off-checklist) at the end of this document is the actionable list. The items most likely to be skipped — because nothing warns you about them — are:\n\n1. The full base chain, not just the concrete class.\n2. Every `catch`, classified: does it mean \"fail\", or a semantic answer? (Sin 8)\n3. Every task input traced to *every* use site — the common defect is resolved at 3 of 4 sites, not 0 of 4.\n4. Caches and memoization: `static` fields, `RegisterTaskObject`, and anything storing a failure.\n5. A Pattern A or Pattern B test that fails when the migration is reverted — asserting on the *result*, not merely that `Execute()` returned true.\n\nIf your host supports agents, the `mt-migration-reviewer` agent automates most of this.\n\n## Migrating Is Not Always the Right Answer\n\nAdding the attribute is a claim that the task is safe to run concurrently in a shared process. When that claim cannot be made honestly, **leaving the task unannotated is a correct, supported outcome** — it simply keeps routing to the TaskHost sidecar, which is exactly the behavior it has today. A slower task is better than a wrong one.\n\nDe-annotate (or never annotate) when:\n\n- The task's semantics depend on being a singleton within the build — `SingleError` above is the canonical case.\n- The task deliberately mutates process-global state for a downstream consumer and cannot be re-architected to pass it via `ProcessStartInfo`.\n- The correctness argument depends on a race being benign and you cannot convince yourself it is.\n\nWhen you do this, leave a comment saying *why*, so the next person does not \"fix\" the missing attribute:\n\n```csharp\n// Deliberately not [MSBuildMultiThreadableTask]: this task must report exactly\n// one error per build, and the Get/RegisterTaskObject pair used to enforce that\n// is not atomic across concurrent instances in a shared node.\n```\n\n---\n\n## Red-Team Audit Protocol\n\n### Phase 1: Trace Every Changed Line\n\nFor each modified line: What was the exact runtime value before? After? Where does it flow (outputs, logs, file paths, dictionary keys)? Does each destination produce identical observable behavior?\n\n### Phase 2: Null/Empty/Edge Input Analysis\n\n| Input | `GetAbsolutePath` | Old behavior | Match? |\n|---|---|---|---|\n| `null` | `ArgumentException` | Varies | ❓ |\n| `\"\"` | `ArgumentException` | Varies | ❓ |\n| `\"C:\\\"` (root) | Valid | Valid | ✅ usually |\n| `\".\"` | `\"C:\\repo\\.\"` (not canonical) | `\"C:\\repo\"` if GetFullPath | ❌ maybe |\n| `\"foo\\..\\bar\"` | `\"C:\\repo\\foo\\..\\bar\"` | `\"C:\\repo\\bar\"` if GetFullPath | ❌ maybe |\n| Already absolute | Pass-through | Pass-through | ✅ |\n\n`Path.GetDirectoryName` → `Path.Combine` chains:\n| Input | `GetDirectoryName` returns | `Path.Combine(result, x)` |\n|---|---|---|\n| `\"C:\\\"` | `null` | Throws `ArgumentNullException` |\n| `\"\"` | `\"\"` (.NET Fx) / `null` (.NET Core+) | Works / Throws ⚠️ |\n| `\"file.resx\"` (no dir) | `\"\"` | Works |\n\nVerify behavior on **both** .NET Framework and .NET TFMs.\n\n### Phase 3: Downstream Impact\n\n1. **Output properties**: What consumes this task's `[Output]`? Does it compare, display, or use as a path?\n2. **Written files**: Does file content change? (e.g., XML with embedded paths)\n3. **Helper methods**: Do `LockCheck`, `ManifestWriter`, etc. internally resolve relative paths?\n4. **Error codes**: MSBuild error codes (MSBxxxx) must be identical.\n\n### Phase 4: Concurrency\n\n1. Two tasks with different `ProjectDirectory` values don't interfere\n2. No writes to static fields (shared across threads)\n3. Every registered task object is reviewed for thread safety, key collisions, registration races, and process-local visibility\n4. No cached *failures* — a failure computed under one task's environment must not be served to another\n5. All file operations use absolutized paths\n\n## Compatibility Test Matrix\n\n```\n[Task] × [Input Type] × [Assertion]\n\nInputs: relative path, absolute path, null, empty, \"..\" segments, root \"C:\\\",\n        forward slashes, trailing separator, UNC path, 260+ char path\n\nAssertions: Execute() return value, [Output] exact string, error message content,\n            exception type, file location, file content\n```\n\n## Sign-Off Checklist\n\n- [ ] `[MSBuildMultiThreadableTask]` on every concrete class (not just base — `Inherited=false`)\n- [ ] **Base chain audited** up to `Task`/`ToolTask` — unannotated bases still run multithreaded and are *not* analyzed\n- [ ] `IMultiThreadableTask` on classes that use `TaskEnvironment` APIs, with default initializer `= TaskEnvironment.Fallback`\n- [ ] Every `[Output]` property: exact string value matches pre-migration (Sin 1)\n- [ ] Every `Log.LogError`/`LogWarning`: path in message matches pre-migration (use `OriginalValue`) (Sin 2)\n- [ ] Every `Log.LogError(ex.Message …)` / `ex.FileName`: exception path sanitized to original input (Sin 2)\n- [ ] Every `GetAbsolutePath` call: null/empty exception behavior matches old code path (Sin 3, 6)\n- [ ] Every dictionary/set with path keys: canonicalization preserved (`GetCanonicalForm()`) (Sin 5)\n- [ ] Every try-catch: absolutized value available in catch block where needed (Sin 4)\n- [ ] Every `??` or `?.` added: verified it doesn't swallow a previously-thrown exception (Sin 3)\n- [ ] No `Path.IsPathRooted` short-circuits around `GetAbsolutePath` — call unconditionally (Sin 7)\n- [ ] Every `catch` classified — any handler meaning a *semantic answer* rather than \"fail\" has all path calls in its `try` absolutized, and is narrowed to the types representing that answer (Sin 8)\n- [ ] **Analyzer-invisible path consumers** absolutized: `AssemblyName.GetAssemblyName`, `XDocument`/`XmlDocument.Load(string)`, `XmlReader.Create(string)`, `ZipFile.*`, `X509CertificateLoader`, `Assembly.LoadFrom` — none of these produce a diagnostic\n- [ ] Every task input resolved at **every** use site, not most of them\n- [ ] Task inputs crossing an interface / delegate / DI boundary (`IFileSystem`, `ICommandFactory`) resolved at the task boundary\n- [ ] No `AbsolutePath` leaks into user-visible strings unintentionally\n- [ ] No behavior conditioned on \"MT mode on/off\" — `TaskEnvironment.Fallback` handles single-process case\n- [ ] **Call chain traced end-to-end** for every helper invoked from `Execute()`:\n    - No `Environment.CurrentDirectory` / `Directory.GetCurrentDirectory()` / `Path.GetFullPath(x)` without a base anywhere in the transitive call graph\n    - No direct `Environment.Get/SetEnvironmentVariable` (route through `TaskEnvironment`)\n    - No `static` mutable fields seeded from process state; replace with `ConcurrentDictionary` keyed on inputs\n    - Every `RegisterTaskObject` use reviewed for object thread safety, key collisions, registration races, and process-local visibility\n    - No cached *failures* whose key omits inputs that affect the result\n    - No `Console.*`, `Environment.Exit`, `Process.Kill`, `FailFast`\n- [ ] ToolTask overrides audited (`GenerateFullPathToTool`, `SkipTaskExecution`, `ValidateParameters`); `ProcessStartInfo.FileName` is absolute, tool *arguments* stay relative\n- [ ] No nested tasks created via `new …Task()` without explicit `TaskEnvironment` propagation\n- [ ] No mutations of engine-owned env vars (e.g. `MSBUILD*` and the framework/SDK discovery vars) in MT mode\n- [ ] Tests for custom tasks set `TaskEnvironment = TaskEnvironmentHelper.CreateForTest()` (built-in tasks have a default)\n- [ ] Migration test follows Pattern A (decoy-CWD) **or** Pattern B (cross-instance independence), or PR explains why no test is meaningful\n- [ ] CWD-mutating tests pinned to a non-parallel xUnit collection\n- [ ] Cross-framework: tested on .NET Framework and .NET TFMs.\n- [ ] If the task was **deliberately left unannotated**, a comment records why (see [Migrating Is Not Always the Right Answer](#migrating-is-not-always-the-right-answer))\n","src/StringTools/README.md":"# Microsoft.NET.StringTools\n\nThis package contains the Microsoft.NET.StringTools assembly which implements common string-related functionality such as weak interning.\n\nAt this time, this is primarily an internal implementation detail of MSBuild and Visual Studio and we do not expect other consumers of the package. If you think you might like to use it, please start a discussion at https://github.com/dotnet/msbuild/discussions to let us know your use cases.\n"},"files":{".github/copilot-instructions.md":"../AGENTS.md",".github/plugin/marketplace.json":"{\n  \"name\": \"msbuild-plugins\",\n  \"owner\": {\n    \"name\": \"dotnet/msbuild\"\n  },\n  \"metadata\": {\n    \"description\": \"GitHub Copilot plugins for the MSBuild ecosystem\",\n    \"version\": \"1.0.0\"\n  },\n  \"plugins\": [\n    {\n      \"name\": \"mt-migration\",\n      \"description\": \"MSBuild multithreaded task migration toolkit: the migration playbook + an MT-specific code reviewer that traces call chains end-to-end.\",\n      \"version\": \"0.1.0\",\n      \"source\": \"./plugins/mt-migration\"\n    }\n  ]\n}\n",".github/skills/assessing-breaking-changes/SKILL.md":"---\nname: assessing-breaking-changes\ndescription: 'Guides assessment of backward compatibility for MSBuild changes. Consult when modifying behavior, adding warnings or errors, changing defaults, altering target ordering, removing or deprecating features, deciding whether a change needs a ChangeWave, reviewing blast radius of behavioral changes, or when a PR introduces user-visible output differences.'\nargument-hint: 'Describe the change and its potential compatibility impact.'\n---\n\n# Backward Compatibility in MSBuild\n\nBackward compatibility is the default — any change that could alter existing build behavior must be explicitly justified.\n\nThis skill covers **how to evaluate compatibility risk**. For the mechanics of ChangeWave implementation, see the [changewaves skill](../changewaves/SKILL.md).\n\n## Core Philosophy\n\n1. **Existing builds must not break.** If a project built successfully yesterday, it must build successfully today with identical semantics.\n2. **New warnings are breaking changes.** Builds that use `-WarnAsError` or `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` will fail if you introduce a new warning. Always gate new warnings behind a ChangeWave or emit them as `Message` importance instead.\n3. **Output changes are behavioral changes.** Even \"improvements\" to output formatting, file paths, or diagnostic text can break downstream consumers that parse MSBuild output.\n4. **Removal is nearly impossible.** Never remove CLI switches, public APIs, or property names. Deprecate with warnings first, then gate removal behind a ChangeWave after multiple release cycles.\n\n## Blast Radius Checklist\n\nBefore merging any behavioral change, evaluate:\n\n| Question | If Yes |\n|----------|--------|\n| Does this change what gets built or how? | ChangeWave required |\n| Does this add a new warning? | ChangeWave required (WarnAsError impact) |\n| Does this change a property default? | ChangeWave required (existing .csproj files depend on defaults) |\n| Does this alter target execution order? | Test with real-world solutions; likely ChangeWave |\n| Does this change console or binlog output format? | Consider downstream tool impact |\n| Does this affect only internal code paths with no user-visible effect? | No ChangeWave needed |\n| Is this a pure bug fix restoring documented behavior? | Usually no ChangeWave; use judgment on blast radius |\n\n## When ChangeWave Is NOT Needed\n\n- Internal refactoring with no observable behavior change\n- Performance improvements that don't change semantics\n- New opt-in features gated by a new property or switch\n- Bug fixes that restore clearly-intended behavior with limited blast radius\n\nFor detailed ChangeWave mechanics, see [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md) and [ChangeWaves.md](../../../documentation/wiki/ChangeWaves.md).\n\n## The Warnings-as-Errors Rule\n\nThis is the most commonly missed compatibility concern:\n\n```xml\n<!-- Many enterprise builds set this globally -->\n<TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n```\n\nAny new `MSBxxxx` warning you add will **break these builds**. Options:\n1. **Gate behind ChangeWave** — preferred for genuinely important warnings\n2. **Use `MessageImportance.Low` or `Normal`** — for informational diagnostics\n3. **Add as an error from the start** — if the condition is always wrong\n\n## Deprecation Protocol\n\n1. Add a deprecation warning (behind ChangeWave if broad impact)\n2. Document the deprecation in release notes\n3. Maintain the old behavior for at least two major .NET versions\n4. Only remove after the ChangeWave has rotated out\n\n## Compatibility Test Matrix\n\nWhen testing backward compatibility, verify:\n\n- **Multi-targeting projects** — `<TargetFrameworks>net472;net8.0</TargetFrameworks>`\n- **Solution builds with mixed project types** — C#, F#, VB, C++/CLI\n- **Incremental builds** — second build should be a no-op\n- **Design-time builds** — Visual Studio calls different target contracts\n- **Cross-platform** — path separator differences, case sensitivity on Linux\n- **WarnAsError builds** — explicitly test with `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>`\n\n## Decision Framework\n\n```\nIs the change user-visible?\n├── No → Ship it (no ChangeWave needed)\n└── Yes\n    ├── Is it a new opt-in feature? → Ship it (no ChangeWave needed)\n    └── Does it change existing behavior?\n        ├── Bug fix with low blast radius? → Ship it, add regression test\n        └── Behavioral change or new warning?\n            └── Gate behind ChangeWave, test opt-out path\n```\n\nDocument the compatibility decision in your PR description so reviewers can validate it.\n",".github/skills/authoring-errors-and-warnings/SKILL.md":"---\nname: authoring-errors-and-warnings\ndescription: 'Guides authoring of MSBuild errors, warnings, and diagnostic messages. Consult when adding new MSBxxxx codes, writing or modifying user-facing diagnostic text, deciding between error/warning/message severity, working with Strings.resx resource files, formatting paths in error output, or evaluating whether a new warning could break WarnAsError builds.'\nargument-hint: 'Describe the error/warning being added or modified.'\n---\n\n# Error and Warning Authoring in MSBuild\n\nError messages are MSBuild's primary user interface. They must help developers fix problems without reading source code.\n\nFor the mechanics of error code assignment, see [assigning-msb-error-code.md](../../../documentation/assigning-msb-error-code.md).\n\n## Error Message Quality Rules\n\nEvery error message must answer three questions:\n\n1. **What happened?** — State the problem clearly\n2. **Why?** — Provide context (file, property, expected value)\n3. **What should the user do?** — Give actionable guidance\n\n```xml\n<!-- BAD: What is \"it\"? What should I do? -->\n<value>MSB4999: Invalid configuration.</value>\n\n<!-- GOOD: States the problem, context, and fix -->\n<value>MSB4999: The project \"{0}\" specifies TargetFramework \"{1}\" which is not installed. Install the SDK for \"{1}\" or update the TargetFramework in the project file.</value>\n```\n\n## Severity Decision Framework\n\n```\nIs the condition always wrong (invalid input, impossible state)?\n├── Yes → ERROR (build should fail)\n└── No\n    ├── Could this cause subtle build correctness issues?\n    │   ├── Yes, likely → WARNING (but see WarnAsError impact below)\n    │   └── Yes, maybe → MESSAGE at Normal importance\n    └── Is this purely informational?\n        └── Yes → MESSAGE at Low importance\n```\n\n### The WarnAsError Constraint\n\n**New warnings are breaking changes** for builds using:\n- `-WarnAsError` (CLI)\n- `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` (project)\n- `<WarningsAsErrors>MSBxxxx</WarningsAsErrors>` (specific codes)\n\nBefore adding a new warning, consider:\n1. **Is the warning important enough to justify breaking WarnAsError builds?** If yes, gate behind a ChangeWave (see [changewaves skill](../changewaves/SKILL.md)).\n2. **Could this be a Message instead?** Messages never break builds but still appear in binary logs.\n3. **Should this be an error from the start?** If the condition is always wrong, skip the warning stage.\n\n## MSB Error Code Assignment\n\n### Code Ranges\n\n| Range | Area |\n|-------|------|\n| `MSB1xxx` | Command-line handling |\n| `MSB3xxx` | Tasks (`Microsoft.Build.Tasks.Core.dll`) |\n| `MSB4xxx` | Engine (`Microsoft.Build.dll`) |\n| `MSB5xxx` | Shared code across assemblies |\n| `MSB6xxx` | Utilities (`Microsoft.Build.Utilities`) |\n\n### Assignment Process\n\n1. Open the `Strings.resx` file for the appropriate assembly\n2. Find the \"Next message code\" comment at the bottom of the file\n3. Search the repo to confirm the code is not already in use\n4. Use the code; update the comment to the next available number\n\nSee [assigning-msb-error-code.md](../../../documentation/assigning-msb-error-code.md) for detailed steps.\n\n## Resource String Format\n\n```xml\n<data name=\"FeatureArea.DescriptiveName\">\n  <value>MSBxxxx: Clear description with {0} placeholders for runtime values.</value>\n  <comment>{StrBegin=\"MSBxxxx: \"}{0} is the project file path. {1} is the property name.</comment>\n</data>\n```\n\n### Rules\n\n- Resource name uses `FeatureArea.DescriptiveName` convention\n- Value starts with `MSBxxxx: ` (code, colon, space)\n- Comment documents the `{StrBegin}` marker and explains each `{N}` placeholder\n- Comments guide translators — mention untranslatable tokens\n\n## Consuming Error Resources in Code\n\nUse the standard formatting method that extracts and applies the error code. Engine-level project\nerrors use `Microsoft.Build.Internal.ProjectErrorUtilities`:\n\n```csharp\n// For errors\nLog.LogErrorWithCodeFromResources(\"Copy.Error\", sourceFile, destFile, ex.Message);\n\n// For warnings\nLog.LogWarningWithCodeFromResources(\"ResolveAssemblyReference.Conflict\", assemblyName);\n\n// For engine-level errors (not in tasks)\nProjectErrorUtilities.ThrowInvalidProject(\n    elementLocation,\n    \"InvalidProjectFile\",\n    arg1, arg2);\n```\n\n**Never** construct error messages by string concatenation — always use resource strings for localization support.\n\n## Localization Requirements\n\n- Error **codes** (`MSBxxxx`) are never localized\n- Error **text** is localized into all supported languages\n- After adding/modifying resource strings, run a full build to generate `.xlf` placeholder translations\n- Use `<comment>` elements to help translators understand context\n- See [Localization.md](../../../documentation/wiki/Localization.md) for the full localization process\n\n## Path Formatting in Error Messages\n\n- Show paths relative to the project directory when possible\n- Use the path exactly as the user specified it (don't normalize slashes)\n- Include file and line number via `IElementLocation` when available — this enables IDE click-to-navigate\n- For paths that could contain spaces, ensure they're quoted in the message\n\n## Checklist for New Errors/Warnings\n\n- [ ] Error code assigned from correct `Strings.resx` range\n- [ ] \"Next message code\" comment updated in the resx\n- [ ] Message answers: what happened, why, what to do\n- [ ] `{StrBegin}` comment and placeholder documentation added\n- [ ] Severity chosen (error vs warning vs message) with WarnAsError considered\n- [ ] If warning: ChangeWave gating evaluated\n- [ ] Full build run to generate `.xlf` files\n- [ ] Test verifies the error is produced with correct code and text\n",".github/skills/changewaves/SKILL.md":"---\nname: changewaves\ndescription: 'Manage MSBuild Change Waves: create new waves, condition features behind opt-out flags, write tests for wave-gated features, document change waves in ChangeWaves.md, and retire expired waves. Use when adding changes that need an opt-out or rotating out old change waves. Changes that introduce a user-visible behavior change should consider whether to use a changewave.'\nargument-hint: 'Add, query, or remove changewaves and changewave checks.'\n---\n\n# Managing MSBuild Change Waves\n\nA Change Wave is an opt-out flag that groups risky features together. Users disable features by setting the environment variable `MSBUILDDISABLEFEATURESFROMVERSION` to the wave version. This skill covers the **how** — the full lifecycle: creating a wave, conditioning code on it, testing, documenting, and retiring.\n\nFor the **when** — deciding whether a change is a breaking change and whether it needs a ChangeWave at all — see [assessing-breaking-changes](../assessing-breaking-changes/SKILL.md).\n\n\n## Decide Whether a Change Wave Is Appropriate\n\nUse a change wave when a change is valuable but has meaningful compatibility risk for existing builds.\n\nGood candidates:\n- User-visible behavior changes that may regress some build graphs.\n- Changes in parsing/evaluation/execution semantics where real-world usage is broad or hard to predict.\n- Changes that may require customer adaptation time and benefit from a temporary opt-out.\n\nUsually avoid a change wave for:\n- Pure bug fixes that restore intended existing behavior with low compatibility risk.\n- Internal refactoring/perf work with no externally observable behavior change.\n- New functionality that is already explicitly opt-in (for example, gated by a new property, switch, or API surface).\n\nIf uncertain, default to safety:\n1. Assess blast radius and rollback difficulty.\n2. Check whether the change could be experienced as a breaking change in production builds.\n3. Use a wave if an opt-out is likely needed while customers adapt.\n\nDocument the decision in PR notes so reviewers can validate the call.\n\nIf a changewave is appropriate, consult [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md) (developer-facing) and [ChangeWaves.md](../../../documentation/wiki/ChangeWaves.md) (public-facing) for details on how to follow the below steps.\n\n## Step 1: Determine the Correct Wave Version\n\nLook up the current MSBuild version. If a wave already exists for that version in `src/Framework/ChangeWaves.cs`, use it. Otherwise, create a new one.\n\n## Step 2: Condition Your Feature on the Wave\n\nC# and MSBuild examples are in [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md). Prefer to put checks inline rather than abstracting out a method to host the check.\n\n## Step 3: Write Tests\n\nWrite normal tests for the new behavior. Then add at least one test that verifies the old behavior is **preserved** when the wave is opted out.\n\n## Step 4: Document the Feature\n\nAdd an entry to the **Current Rotation of Change Waves** section in [ChangeWaves.md](../../../documentation/wiki/ChangeWaves.md).\n\n- If a heading for this wave already exists, add a bullet under it.\n- If not, add a new `### {Major}.{Minor}` heading at the top of the current rotation list.\n\nEach entry is a bullet with a link to the PR:\n\n```markdown\n### 18.6\n- [Short description of the change.](https://github.com/dotnet/msbuild/pull/NNNNN)\n```\n\n## Only when explicitly requested: Retire an Expired Wave\n\nWaves rotate out when a new major .NET version will be released. When retiring a wave, follow the detailed description in [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md).\n\n## Checklist\n\n- [ ] Decision made: change wave needed?\n- [ ] Correct wave version chosen from `eng/Versions.props`\n- [ ] Wave field and `AllWaves` entry added in `ChangeWaves.cs` (if new wave)\n- [ ] Feature code wrapped with `AreFeaturesEnabled`\n- [ ] Test verifying opt-out disables the feature\n- [ ] Entry added to `documentation/wiki/ChangeWaves.md`\n",".github/skills/cswin32-com/SKILL.md":"---\nname: cswin32-com\ndescription: 'Guides struct-based COM interop in MSBuild using CsWin32 patterns. Consult when working with the generic ComScope type, ComClassFactory, IComIID, the IID.Get generic method, delegate* unmanaged vtables, CoCreateInstance, or manually defining COM interfaces not in Win32 metadata (e.g. WMI IWbemLocator, IWbemServices).'\nargument-hint: 'Describe the COM interface or activation pattern you are working with.'\n---\n\n# CsWin32 COM Interop Guide\n\nStruct-based COM interop using CsWin32 patterns — AOT-compatible, no `[ComImport]` or built-in marshalling.\n\n**Paired skill:** [cswin32-interop](../cswin32-interop/SKILL.md) covers general P/Invoke and the blittable signature rules that apply to both `[DllImport]` and COM vtables. This file covers only the COM-specific layer.\n\n## Workflow\n\n1. **Interface in Win32 metadata?** Add the name to `src/Framework/NativeMethods.txt` → CsWin32 generates it. Not in metadata (WMI, Fusion, Setup Configuration) → define a manual struct under its own folder, excluded from source builds via `<Compile Remove>`.\n2. **Lifetime: `using ComScope<T> scope = new();`** for every transient COM pointer (`CoCreateInstance`, `QueryInterface`, `IEnumXxx::Next`, factory output, app-local `STDAPI Get*`, etc.). Never write `T* local; try { ... } finally { local->Release(); }` — that's the pre-`ComScope` shape that leaks on every early return. Same for `BSTR` out-params: `using BSTR x = default;`.\n3. **Activate** via `ComClassFactory.TryCreate(CLSID, ...)` (AOT-compatible) or `PInvoke.CoCreateInstance` with `IID.Get<T>()` — not `&localGuid`.\n4. **Call** via `scope.Pointer->Method(...)`. Pass `ComScope<T>` directly where the API expects `T**` / `void**` — the implicit operator does the address-of.\n5. **Match the caller's error contract.** If the top-level consumer swallows COM failure (\"no result\" == \"absent\"), helpers return `default` / `false` instead of throwing a `COMException` that's immediately discarded. `ThrowOnFailure` only when the exception will actually propagate or assert a should-never-happen.\n6. **Guard with `#if FEATURE_WINDOWSINTEROP`** — add `&& NET` only when the struct uses the static-abstract `IComIID` form exclusively (no net472 dual-target).\n\n## Manual COM Structs (Not in Metadata)\n\nDefine each interface in its own file under e.g. `src/Tasks/AssemblyDependency/Fusion/`, `src/Framework/Shared/VisualStudio/`, `src/Framework/Utilities/Wmi/`. Exclude from source builds. Pattern (dual-target — works on net472 + .NET):\n\n```csharp\ninternal unsafe struct IAssemblyCache : IComIID\n{\n    public static readonly Guid IID_IAssemblyCache = new(0xE707DCDE, ...);\n\n#if NET\n    static ref readonly Guid IComIID.Guid\n    {\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        get => ref Unsafe.AsRef(in IID_IAssemblyCache);\n    }\n#else\n    readonly ref readonly Guid IComIID.Guid\n    {\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        get => ref Unsafe.AsRef(in IID_IAssemblyCache);\n    }\n#endif\n\n    private readonly void** _lpVtbl;\n\n    // IUnknown at indices 0-2, then interface methods at 3+\n    public HRESULT QueryInterface(Guid* riid, void** ppvObject)\n    {\n        fixed (IAssemblyCache* pThis = &this)\n            return ((delegate* unmanaged[Stdcall]<IAssemblyCache*, Guid*, void**, HRESULT>)_lpVtbl[0])(pThis, riid, ppvObject);\n    }\n    // AddRef @1, Release @2, then interface methods at correct slot indices.\n}\n```\n\n**Rules:**\n- `delegate* unmanaged[Stdcall]` for the function-pointer cast — IDL `STDMETHODCALLTYPE` ⇒ `__stdcall` on Win32. Picking the wrong convention silently corrupts the stack.\n- `IComIID` has two shapes: static-abstract (`static ref readonly Guid IComIID.Guid`) on .NET 7+, instance (`readonly ref readonly Guid IComIID.Guid`) on net472 / netstandard2.0. CsWin32 picks the right one for its generated structs automatically (see [IComIID across TFMs](#icomiid-across-tfms)); a **manual** struct must spell out both arms itself. For .NET-only structs (WMI), drop the `#else` branch and gate the whole file `#if NET`. For dual-target structs (Fusion), keep both as shown — see `src/Tasks/AssemblyDependency/Fusion/` for the canonical example.\n- Use CsWin32-generated `PCWSTR` / `PWSTR` for wide strings (add to `NativeMethods.txt`); raw `char*` only when no typed equivalent exists.\n- Vtable slots are exact: index 0 = `QueryInterface`, 1 = `AddRef`, 2 = `Release`, 3+ = interface methods in IDL order. When inheriting, **add the parent interface's method count** before counting your own (e.g. `ISetupConfiguration2.EnumAllInstances` is at slot 6 = 3 IUnknown + 3 v1 + 0).\n- CS0592 prevents `[SupportedOSPlatform]` on structs — put it on individual methods.\n\n## Lifetime & Access\n\n**A raw `T*` (COM struct) is forbidden as a field on a non-`ref` type.** Allowed locations: locals in `unsafe` methods, parameters, fields of a `ref struct`. Anywhere else (instance fields of `class` / non-`ref` `struct`) → use `AgileComPointer<T>` — a raw pointer field is an apartment-agility hazard and leaks on finalize-without-Dispose.\n\n### `ComScope<T>` — transient pointers\n\n`ref struct`, `using`'d, calls `Release` on dispose. **The default for everything that doesn't survive the current method.** Implicitly converts to `T**` and `void**`, so out-params write into the scope directly:\n\n```csharp\nusing ComScope<ISomeInterface> scope = new();\nGuid clsid = SomeStruct.CLSID;\nGuid iid = IID.Get<ISomeInterface>();\nPInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &iid, scope).ThrowOnFailure();\nscope.Pointer->DoThing(...);\n\nusing ComScope<IOther> other = new();\nGuid otherIid = IOther.IID_IOther;\nscope.Pointer->QueryInterface(&otherIid, other).ThrowOnFailure();\n```\n\n- Access methods via `scope.Pointer->Method(...)`. Null-check with `scope.IsNull`.\n- Applies to *every* COM out-param: `CoCreateInstance`, `QueryInterface`, `IEnumXxx::Next`, factory methods, app-local `STDAPI Get*` (declare the `[DllImport]` with `T** pp`, not `out T*`, so the implicit operator binds).\n\n**Ownership transfer out of a helper.** A helper that acquires a COM pointer can return `ComScope<T>` directly; intermediate pointers stay in their own `using` scopes and Release on the helper's `return`. `default` `ComScope<T>` is null and its `Dispose` is a no-op, so callers don't need an extra null guard:\n\n```csharp\nprivate static ComScope<ISetupConfiguration2> AcquireSetupConfig()\n{\n    Guid clsid = SetupConfiguration.CLSID_SetupConfiguration;\n    Guid iid = ISetupConfiguration2.IID_ISetupConfiguration2;\n\n    ComScope<ISetupConfiguration2> config = new();\n    HRESULT hr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &iid, config);\n    if (hr.Succeeded) return config;                 // ownership flows to caller\n    if (hr != HRESULT.REGDB_E_CLASSNOTREG) return default;\n\n    using ComScope<ISetupConfiguration> v1 = new();  // intermediate, Released on return\n    if (SetupConfiguration.GetSetupConfiguration(v1, IntPtr.Zero) < 0 || v1.IsNull) return default;\n    return v1.Pointer->QueryInterface(&iid, config).Succeeded ? config : default;\n}\n\n// caller:\nusing ComScope<ISetupConfiguration2> config = AcquireSetupConfig();\nif (!config.IsNull) { /* use config.Pointer */ }\n```\n\n### `BSTR` — COM strings\n\nCsWin32-generated; extended in [`Windows/Win32/Foundation/BSTR.cs`](../../../src/Framework/Windows/Win32/Foundation/BSTR.cs) with `IDisposable`. `Dispose` calls `SysFreeString` and zeroes the field in place. **Scope every COM `BSTR` out-param with `using BSTR x = default;`** — no manual `try/finally + SysFreeString`:\n\n```csharp\nusing BSTR versionBstr = default;\nif (instance->GetInstallationVersion(&versionBstr).Failed) return false;\nstring version = versionBstr.ToString();\n// SysFreeString runs at scope exit.\n```\n\nIn-place-only: `BSTR.Dispose` writes through `Unsafe.AsRef(in this)`, so `using` must be on the storage location, not on a method-returned copy. Same rule as `ComScope<T>`.\n\n### `AgileComPointer<T>` — fields outliving a method\n\nFinalizable managed class. Use whenever a COM pointer is stored in a class field (the only legal way — raw `T*` fields are forbidden, see top of section).\n\n- Registers in the Global Interface Table (thread-agile); finalizer releases if `Dispose` is missed.\n- Access via `using ComScope<T> scope = agile.GetInterface();` then `scope.Pointer->Method(...)`. `GetInterface()` round-trips through the GIT — hoist a single scope to the top of a method when several calls share it.\n- Constructor `takeOwnership`: pass `false` when the raw pointer is also held by a `ComScope<T>` that will Release (GIT AddRefs independently); pass `true` only when no other code path will Release.\n- Dispose via the owner's `Dispose` / `DisposeManagedResources` (`AgileComPointer` is managed, not unmanaged — its finalizer is the safety net, not the primary cleanup).\n\n## Activation\n\n```csharp\n// AOT-compatible\nif (ComClassFactory.TryCreate(IWbemLocator.CLSID, out var factory, out HRESULT hr))\n    using ComScope<IWbemLocator> instance = factory.TryCreateInstance<IWbemLocator>(out hr);\n\n// CoCreateInstance — IID.Get<T>(), not &localGuid\nGuid clsid = IWbemLocator.CLSID;\nusing ComScope<IWbemLocator> locator = new();\nhr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.Get<IWbemLocator>(), locator);\n```\n\n## Error-Handling Parity When Migrating\n\nStruct-based COM returns raw `HRESULT`; `[ComImport]` and built-in activation threw automatically. Preserve the old throw-vs-return at each call site:\n\n| Old shape | Threw via | Migrated shape |\n|---|---|---|\n| `new SomeCoClass()` | built-in activation | `PInvoke.CoCreateInstance(...).ThrowOnFailure()` |\n| `(IFoo)rcw` cast | `InvalidCastException` on QI | `QueryInterface(&iid, scope).ThrowOnFailure()` |\n| `[ComImport]` method, default `PreserveSig=false` | marshaller throws on `FAILED(hr)` | `.ThrowOnFailure()` at the call site |\n| `[ComImport]` method with `[PreserveSig]` | caller inspects return | mirror the existing `hr` branch — don't start throwing |\n\n**Factory-method exception:** when the old contract was \"return null for invalid input\" (e.g. `Create(path)`), keep null-return only for the legitimate-rejection path; activation / QI failures still throw. See [`MetadataReader.cs`](../../../src/Tasks/ManifestUtil/MetadataReader.cs) (throws on activation/QI, returns null on `OpenScope`).\n\n**Don't throw when the caller swallows it.** If the top-level consumer wraps the whole chain in `catch (COMException) { }`, inner helpers must return `default` / `false` / empty instead of constructing a `COMException` only to have it discarded. Throwing here allocates the exception, walks the stack, and hides the HRESULT for no benefit. See [`VisualStudioLocationHelper.AcquireSetupConfiguration2`](../../../src/Framework/VisualStudioLocationHelper.cs).\n\n## Mocking Struct-Based COM in Tests\n\nStruct-based COM calls go through raw vtable pointers, so a managed mock can't be passed where a `T*` is expected. Bridge with the **built-in COM marshaller**: the mock implements the *managed* interface (BCL `System.Runtime.InteropServices.ComTypes.*` when ABI-compatible — true for `ITypeLib` / `ITypeInfo` — else CsWin32's nested `[ComImport] internal interface Interface`), and the test passes a **CCW** (COM-callable wrapper) pointer to the code under test:\n\n```csharp\n// MockTypeLib : ComTypes.ITypeLib   (a normal managed class)\nIntPtr ccw = Marshal.GetComInterfaceForObject(mock, typeof(ComTypes.ITypeLib));\ntry { walker.AnalyzeTypeLibrary((ITypeLib*)ccw); }   // struct API; real QueryInterface works\nfinally { Marshal.Release(ccw); }\n```\n\nThe CCW exposes a real vtable, so no hand-rolled native vtable is needed and the managed mock framework stays intact. But the mock must now behave like real COM, not a managed object:\n\n- **All `[out]` params are written**, even ones the caller ignores. Passing `null` for a trailing out-param (`GetDocumentation(-1, &name, null, null, null)`) faults the marshaller — supply a real pointer for each.\n- **Exception identity is lost.** An HRESULT-returning method resurfaces a thrown exception as a *new* `COMException` (HRESULT preserved, instance not); a `uint`/`void` method with no HRESULT channel (`GetTypeInfoCount`, the `Release*` methods) **swallows it silently** and returns garbage. Assert on type/count, not the injected instance, and don't rely on `void`/`uint` methods to propagate.\n- **Throw `COMException` on bad input, don't `Assert`.** An `Assert` throws an `XunitException` that escapes the SUT's `catch (COMException)`; a `COMException` becomes a failing HRESULT the SUT handles normally. (Matters because a swallowed `GetTypeInfoCount` failure can feed an out-of-range index to the next call.)\n\nSee [`MockTypeLib.cs`](../../../src/Tasks.UnitTests/MockTypeLib.cs) / [`MockTypeInfo.cs`](../../../src/Tasks.UnitTests/MockTypeInfo.cs) and the CCW bridge in [`ComReferenceWalker_Tests.cs`](../../../src/Tasks.UnitTests/ComReferenceWalker_Tests.cs).\n\n## Strongly-typed handle / token wrappers\n\nWhen the native side `typedef`s a primitive into a family of \"same shape, different meaning\" aliases (`corhdr.h`'s `typedef ULONG32 mdToken; typedef mdToken mdAssembly; ...`), mirror that hierarchy with distinct `readonly struct` wrappers holding the single underlying primitive — same pattern as CsWin32's `HANDLE` / `HWND` / `HMODULE`. Stays blittable; `delegate*` casts and arrays marshal at zero cost.\n\nConversions follow the typedef hierarchy: **implicit** widening to the base (`MdAssembly` → `MdToken`, always safe), **explicit** narrowing (`(MdAssembly)token`, opt-in because the C side can't enforce the kind at the cast site).\n\n**Check the native header for the canonical validation primitives** before defining `IsNil` / `IsValid`. Encoding details often hide in macros that don't grep cleanly. For `mdToken` the encoding is `(TableType << 24) | Rid`:\n\n| C macro | Meaning |\n|---|---|\n| `TypeFromToken(tk) = tk & 0xff000000` | Table-type tag (high byte) → `CorTokenType` |\n| `RidFromToken(tk) = tk & 0x00ffffff`  | Row id (low 24 bits) |\n| `IsNilToken(tk) = RidFromToken(tk) == 0` | Nil check — **rid half, not the whole value** |\n| `mdAssemblyNil = mdtAssembly = 0x20000000` | Per-type nil is the table-type tag, **not 0** |\n\nA naive `IsNil => Value == 0` would misclassify the `0x20000000` \"no assembly in this scope\" return from `GetAssemblyFromScope`. Mirror the macro: `IsNil => Rid == 0`. Reference: [`Tokens.cs`](../../../src/Tasks/AssemblyDependency/Metadata/Tokens.cs), [`CorTokenType.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorTokenType.cs).\n\n## IComIID across TFMs\n\nCsWin32 (since **0.3.287**) emits `IComIID` and attaches it to every generated COM struct on **all** target frameworks — static-abstract `static ref readonly Guid Guid { get; }` on .NET 7+, instance `ref readonly Guid Guid { get; }` on net472 / netstandard2.0. No hand-authored polyfill is needed; the old `src/Framework/Polyfills/IComIID.cs` + `IComIIDPolyfills.cs` files (one `partial` per generated struct) are gone.\n\nWhen you add a new CsWin32-generated COM type to `NativeMethods.txt`, it carries `IComIID` automatically through `ComScope<T>` — nothing extra to do.\n\nOnly **manual** structs (WMI, Fusion, Setup Configuration, CLR metadata) implement `IComIID` by hand, matching the per-TFM shape — see [Manual COM Structs](#manual-com-structs-not-in-metadata). `IID.Get<T>()` ([`IID.cs`](../../../src/Framework/Windows/Win32/IID.cs)) reads it via `T.Guid` on .NET and `default(T).Guid` on net472.\n\n## File Organization\n\n| Location | Contents |\n|---|---|\n| `src/Framework/Windows/Win32/System/Com/` | `ComScope`, `ComClassFactory`, `AgileComPointer`, `GlobalInterfaceTable` |\n| `src/Framework/Windows/Win32/IID.cs` | Generic IID lookup |\n| `src/Framework/Utilities/Wmi/`, `src/Framework/Shared/VisualStudio/`, `src/Tasks/AssemblyDependency/Fusion/`, `src/Tasks/AssemblyDependency/Metadata/`, `src/Tasks/TypeLib/` | Manual COM struct interfaces — own folder per API surface |\n\n## CS3016 CLS Compliance\n\nCsWin32 (since **0.3.287**) auto-emits `[CLSCompliant(false)]` on every generated COM struct wrapper that carries CCW thunks (the `[UnmanagedCallersOnly(CallConvs = new[] { ... })]` array argument is what trips CS3016), and adds CS3019 / CS3021 to its own generated-file `#pragma warning disable` list. So a CLS-compliant assembly (`[assembly: CLSCompliant(true)]`) builds clean with no consumer action — the hand-authored `GeneratedInteropClsCompliance.cs` partial and the `.editorconfig` CS3019 suppression for `{**/Windows/**/*.cs}` are gone.\n\nThe attribute is only emitted on .NET 5+ TFMs (net472 / netstandard2.0 have no CCW thunks, so no CS3016) and only for `internal` wrappers (the generator default). Don't re-add per-type `[CLSCompliant(false)]` partials or blanket `NoWarn`. See https://github.com/dotnet/roslyn/issues/68526 for the underlying Roslyn limitation that motivated the generator-side fix.\n",".github/skills/cswin32-interop/SKILL.md":"---\nname: cswin32-interop\ndescription: 'Guides CsWin32 P/Invoke interop in MSBuild. Consult when working with the PInvoke class, Windows.Win32 namespaces, FEATURE_WINDOWSINTEROP, HANDLE/HMODULE/HRESULT types, the generic BufferScope type, replacing [DllImport] with CsWin32, or conditioning Windows-only code for source builds.'\nargument-hint: 'Describe the Windows API or interop code you are migrating or adding.'\n---\n\n# CsWin32 Interop Guide\n\n[CsWin32](https://github.com/microsoft/CsWin32) replaces `[DllImport]` with source-generated `PInvoke.*` calls. `FEATURE_WINDOWSINTEROP` is the compile-time gate; source builds disable it.\n\n**Paired skill:** [cswin32-com](../cswin32-com/SKILL.md) covers struct-based COM interop on top of CsWin32 (`ComScope<T>`, `AgileComPointer<T>`, `delegate* unmanaged` vtables, `IComIID`, `CoCreateInstance`, manual COM structs not in Win32 metadata). This file covers only the general P/Invoke layer; the COM skill builds on its blittable-signature rules.\n\n## Rules\n\n1. **Replace `[DllImport]` with `PInvoke.*`**. Delete old declarations and hand-written structs/enums/constants.\n2. **Gate with `#if FEATURE_WINDOWSINTEROP`**, add runtime `IsWindows` check inside. Both required.\n3. **Use CsWin32 types directly** (`HANDLE`, `HMODULE`, `HRESULT.S_OK`, `FILE_FLAGS_AND_ATTRIBUTES`, etc.).\n4. **Call `PInvoke.*` directly** — no wrappers. Types flow via `InternalsVisibleTo`.\n5. **Prefer CsWin32 for Windows APIs**. Use `[LibraryImport]` only for non-Windows native calls (e.g. `libc`), guarded with `#if NET`.\n6. **Preserve the old error-handling contract.** Check the original `[DllImport]` for `PreserveSig` / `SetLastError` / `BOOL` / `HRESULT` semantics and reproduce them: `PreserveSig=false` → `.ThrowOnFailure()`; `SetLastError=true` + failed `BOOL` → `throw new Win32Exception()`. Silently returning where the old code threw is a behavior change. See [cswin32-com's parity table](../cswin32-com/SKILL.md#error-handling-parity-when-migrating) for the COM-side equivalent.\n\n## Blittable signatures\n\nCsWin32 is configured with `allowMarshaling: false`, so every `[DllImport]` and every manual\nCOM vtable method must be blittable — no marshalling at the boundary. These rules apply\nto both. For COM-vtable-only additions, see\n[cswin32-com](../cswin32-com/SKILL.md#blittable-vtable-signatures).\n\n- **Return `HRESULT`** from HRESULT-returning APIs (not `int`). Blittable (single `int` field),\n  exposes `.Succeeded` / `.Failed` / `.ThrowOnFailure()`. Use `HRESULT.S_OK` over `0`; cast\n  `e.HResult` to `(HRESULT)` when wrapping. `AddRef` / `Release` return `uint`.\n- **Call `.ThrowOnFailure()`** instead of `if (hr.Failed) Marshal.ThrowExceptionForHR(hr)` —\n  same exception, IErrorInfo-enriched, one-line call site:\n  `iface->Method(...).ThrowOnFailure();`. Branch on `hr` only when handling a specific HRESULT\n  (e.g. `ERROR_INSUFFICIENT_BUFFER`) before throwing. See\n  [cswin32-com's parity table](../cswin32-com/SKILL.md#error-handling-parity-when-migrating)\n  for the migration contract.\n- **Use `PCWSTR` / `PWSTR`** for wide strings, never managed `string`. Implicit conversion from\n  `fixed (char* p = managedString)`. Add to `NativeMethods.txt` if not yet generated.\n- **`T**` not `out T*`** for pointer outputs. `out` triggers marshaling + a `fixed` round-trip\n  at every call site.\n- **`void*` for opaque / reserved params** — never `IntPtr.Zero`; pass `null` literally.\n  `IntPtr` is fine at boundaries with the wider .NET surface (`Marshal.*`,\n  `SafeHandle.DangerousGetHandle`, public API).\n- **Prefer `nint` / `nuint`** over `IntPtr` / `UIntPtr` for native-sized integers — no boxing,\n  better cast semantics, no `IntPtr.Zero` ceremony.\n- **No managed reference types** (`string`, `StringBuilder`, arrays) in blittable signatures.\n- **Don't specify `PreserveSig = true` on `[DllImport]`** — it's the default. Use\n  `PreserveSig = false` only to force marshaller throw-on-failure (rare; prefer returning\n  `HRESULT` and `.ThrowOnFailure()`). `[ComImport]` defaults the opposite way, but struct-based\n  COM uses raw `delegate*` and isn't affected — see\n  [cswin32-com](../cswin32-com/SKILL.md#blittable-vtable-signatures).\n- **Constrain flag / option parameters to a typed `enum`.** When a native `DWORD` / `ULONG` /\n  `int` is documented as a `typedef enum` or `#define` set, declare a C# `[Flags] enum Foo : uint`\n  (matching the underlying type) and use it in the signature (and `delegate*` cast for COM).\n  Mirror the constraint even when the native side has no named enum. Self-documenting at the\n  call site: `OpenScope(path, CorOpenFlags.ofRead, ...)` vs `OpenScope(path, 0, ...)`. Co-locate\n  the enum next to its consumer. See\n  [`CorOpenFlags.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorOpenFlags.cs) and\n  [`CorAssemblyFlags.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorAssemblyFlags.cs).\n\n### Dual Guard Pattern\n\n```csharp\n#if FEATURE_WINDOWSINTEROP\n    if (IsWindows)\n    {\n        PInvoke.GetFileAttributesEx(fullPath, out WIN32_FILE_ATTRIBUTE_DATA data);\n    }\n#endif\n    // Cross-platform fallback\n```\n\n**WRONG**: `if (IsWindows) { #if FEATURE_WINDOWSINTEROP ... #endif }` — dead code in source builds.\n\nWindows-only files are excluded via `<Compile Remove>` instead — no `#if` inside needed.\n\n## Infrastructure\n\n**Define**: `src/Directory.BeforeCommon.targets` sets `FEATURE_WINDOWSINTEROP` + `$(FeatureWindowsInterop)` when `DotNetBuildSourceOnly != true`. Use `$(FeatureWindowsInterop)` in `.csproj` for `<Compile Remove>`/`<Compile Include>`.\n\n**CsWin32 config**: `src/Framework/NativeMethods.txt` (API list) + `NativeMethods.json` (`allowMarshaling: false`, `useSafeHandles: false`). Lives in Framework; other projects consume via `InternalsVisibleTo`. Do not add CsWin32 to other projects.\n\n**Guard selection**:\n\n| Guard | When | Runtime check? |\n|-------|------|----------------|\n| `#if FEATURE_WINDOWSINTEROP` | Multi-TFM Windows calls | Yes |\n| `#if FEATURE_WINDOWSINTEROP && NET` | Manual COM structs gated .NET-only (e.g. WMI). CsWin32-generated COM types via `ComScope<T>` work on net472 too — the generator emits `IComIID` on every TFM (see [cswin32-com](../cswin32-com/SKILL.md#icomiid-across-tfms)) | Yes |\n| `#if FEATURE_WINDOWSINTEROP && !NETSTANDARD` | CsWin32 types without `static abstract` (net472 + net10) | Yes |\n| `#if !NET` / `#if FEATURE_MSCOREE` | net472-only = inherently Windows | No |\n\n**Namespace imports** must be inside `#if FEATURE_WINDOWSINTEROP`. WDK APIs use `Windows.Wdk` namespace.\n\n**Files**: `src/Framework/Windows/` (CsWin32 partials), `src/Shared/Win32/` (COM helpers), `src/Framework/Utilities/Wmi/` (.NET-only COM structs).\n\n### Constant Replacements\n\n`NativeMethodsShared.S_OK` → `HRESULT.S_OK`, `InvalidHandle` → `HANDLE.INVALID_HANDLE_VALUE`, `FILE_ATTRIBUTE_DIRECTORY` → `FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY`, `STD_OUTPUT_HANDLE` → `STD_HANDLE.STD_OUTPUT_HANDLE`, `GENERIC_READ` → `FILE_ACCESS_RIGHTS.FILE_GENERIC_READ`. Pattern: `CsWin32EnumType.ORIGINAL_NAME` — check generated types in `obj/`.\n\n**Always prefer the generated enum over a local copy.** Before defining `private const int ERROR_*` / `private enum FooFlags`, grep the CsWin32 metadata:\n\n- `ERROR_*` (Win32 error codes) → `WIN32_ERROR.ERROR_*` (uint enum)\n- `HRESULT` codes → `HRESULT.S_OK`, etc.\n- Restart Manager status/type → `RM_APP_STATUS` / `RM_APP_TYPE`\n- File flags → `FILE_FLAGS_AND_ATTRIBUTES`, `FILE_ACCESS_RIGHTS`, `FILE_SHARE_MODE`, `FILE_CREATION_DISPOSITION`\n- Process flags → `PROCESS_CREATION_FLAGS`, `STARTUPINFOW_FLAGS`, `PROCESS_ACCESS_RIGHTS`\n- Memory mapping → `PAGE_PROTECTION_FLAGS`, `FILE_MAP`\n- Shell folder → `KNOWN_FOLDER_FLAG`\n\nCast `int`/`uint` return codes via `(WIN32_ERROR)res` for `switch` and equality. Add the enum to `NativeMethods.txt` if not yet generated, then check `obj/.../generated/Microsoft.Windows.CsWin32/.../Windows.Win32.<EnumName>.g.cs`.\n\n**Some flag values are standalone constants, not enum members.** A Win32 `#define` outside a `typedef enum` generates as an `internal const` on `PInvoke`. Example: the `LoadTypeLibEx` flags are `PInvoke.LOAD_TLB_AS_32BIT` / `_64BIT` (`uint`), **not** members of `REGKIND` (which has only `REGKIND_DEFAULT/REGISTER/NONE`). Add the *constant name* to `NativeMethods.txt` like any API. OR it onto an enum at the constant's width and cast back: `(REGKIND)((uint)REGKIND.REGKIND_NONE | PInvoke.LOAD_TLB_AS_32BIT)`. Don't reintroduce a local `const` CsWin32 already emits.\n\n**Match local types to the CsWin32 type.** Instead of `int res = (int)PInvoke.RmStartSession(...)` and casting at every comparison, declare `WIN32_ERROR res = PInvoke.RmStartSession(...)` and let helpers like `GetException(WIN32_ERROR res, ...)` take the typed value. Cast to `int`/`uint` only at the boundary where a non-CsWin32 API needs it (e.g. `new Win32Exception((int)res, ...)`). The same applies to `HRESULT`, `BOOL`, `HANDLE`, `PROCESS_CREATION_FLAGS`, etc.\n\n**Delete local mirror enums** that exist solely to mirror the Win32 one. The generated CsWin32 type is the source of truth.\n\n### FILETIME Conversions\n\nUse the helpers in [`src/Framework/Windows/Win32/Foundation/FileTimeExtensions.cs`](../../../src/Framework/Windows/Win32/Foundation/FileTimeExtensions.cs):\n\n- `fileTime.ToLong()` → 64-bit ticks\n- `fileTime.ToDateTime()` → local `DateTime` (FILETIME values returned as local time, e.g. `RM_PROCESS_INFO.ProcessStartTime`)\n- `fileTime.ToDateTimeUtc()` → UTC `DateTime` (FILETIME values returned as UTC, e.g. `WIN32_FILE_ATTRIBUTE_DATA.ftLastWriteTime`)\n\nDo **not** hand-roll `DateTime.FromFileTime((long)hi << 32 | lo)` — use the helpers for consistency. Note CsWin32-generated structs use `ComTypes.FILETIME` (int fields) for COM members and `Windows.Win32.Foundation.FILETIME` (uint fields) for kernel ones; the extension covers `ComTypes.FILETIME`.\n\n## BufferScope<T>\n\n`BufferScope<T>` (`src/Framework/Utilities/BufferScope.cs`) — stackalloc initial buffer with `ArrayPool<T>` fallback. Lives in Framework, available to all projects via `InternalsVisibleTo`.\n\n```csharp\nusing BufferScope<char> buffer = new(stackalloc char[(int)PInvoke.MAX_PATH]);\nint length = (int)PInvoke.GetShortPathName(path, buffer.AsSpan());\nif (length > buffer.Length)\n{\n    buffer.EnsureCapacity(length);\n    length = (int)PInvoke.GetShortPathName(path, buffer.AsSpan());\n}\nif (length > 0) path = buffer.Slice(0, length).ToString();\n```\n\n- `ref struct` — always use with `using`. Never stack-allocate more than 1024 bytes.\n- Check CsWin32 convenience overloads (e.g. `GetShortPathName(string, Span<char>)`) before writing `fixed` blocks.\n\n## Gotchas\n\n### CA1416 Platform Compatibility\n\nNo blanket `NoWarn` — handle semantically:\n- `if (IsWindows)` satisfies `[SupportedOSPlatform]` — no pragma needed\n- `if (IsUnixLike)` satisfies `[UnsupportedOSPlatform(\"windows\")]`\n- **Never use `!IsWindows`** — use `else if (IsUnixLike)`. See `documentation/specs/CA1416-analyzer-analysis.md`\n- Use versioned `[SupportedOSPlatform(\"windows6.1\")]` on methods calling CsWin32 APIs\n- `#pragma warning disable CA1416` only for **static local functions** (analyzer limitation)\n- CS0592 prevents `[SupportedOSPlatform]` on `partial struct` — put on individual members instead\n\n### Type Conversions\n\n- `HANDLE ↔ IntPtr`: `(HANDLE)intPtr` / `(IntPtr)h.Value`. Sentinels: `HANDLE.Null`, `HANDLE.INVALID_HANDLE_VALUE`\n- `FILETIME` conversion: `data.ftLastWriteTime.ToLong()`, `.ToDateTime()` (local), `.ToDateTimeUtc()` — see \"FILETIME Conversions\" above. CsWin32 uses `ComTypes.FILETIME` (int fields), not `Win32.Foundation.FILETIME`\n- `SafeFileHandle`: `new SafeFileHandle((IntPtr)h.Value, true)`, pass with `(HANDLE)handle.DangerousGetHandle()`\n- Nullable structs: `(SECURITY_ATTRIBUTES?)null`\n- Enum flags: use bitwise `&` — `HasFlag()` boxes on .NET Framework\n- Anonymous unions: `systemInfo.Anonymous.Anonymous.wProcessorArchitecture` — check generated source in `obj/`\n\n### Source-Build Verification (REQUIRED before pushing)\n\nSource builds (`DotNetBuildSourceOnly=true`) disable `FEATURE_WINDOWSINTEROP`. CI treats **all warnings as errors**. Run both builds before every push:\n\n```shell\n# Normal build\ndotnet msbuild MSBuild.Dev.slnf -v:q\n\n# Source-build — catches unused usings/members/docs from #if guards\ndotnet msbuild MSBuild.SourceBuild.slnf /p:DotNetBuildSourceOnly=true -v:q\n```\n\n**Everything** only referenced inside `#if FEATURE_WINDOWSINTEROP` must also be guarded:\n- **IDE0005**: `using` directives — most common failure\n- **IDE0051/IDE0052**: Private members (methods, fields, including helpers like `StringToByteArray`, constants like `ERROR_SHARING_VIOLATION`)\n- **CA1823**: Unused private fields (e.g. constants only consumed inside the guard)\n- **CS1587**: XML doc comments (move inside `#if`, not before)\n\nThe same applies when adding **polyfills** in `src/Framework/Polyfills/` (e.g. `SpanExtensions`, `IndexOfAnyExcept`): polyfills usually live behind `#if !NET` (or similar TFM guards) but are still consumed from `#if FEATURE_WINDOWSINTEROP` code paths. Always run the source-build to confirm the polyfill, its callers, and any helper members compile cleanly when interop is disabled — a polyfill referenced only by Windows-only code will trip IDE0051/CA1823 in source-only builds.",".github/skills/deploy-msbuild-to-vs/SKILL.md":"---\nname: deploy-msbuild-to-vs\ndescription: Deploy locally-built MSBuild binaries into a Visual Studio installation for testing and debugging. Use when you need VS itself to use your local MSBuild changes, or when debugging MSBuild as invoked by VS.\nargument-hint: Deploy local MSBuild to VS, patch VS with built MSBuild, debug MSBuild inside VS.\n---\n\n# Deploying Built MSBuild into Visual Studio\n\nThis skill guides you through replacing Visual Studio's bundled MSBuild with your locally-built binaries, so that builds initiated from within VS use your changes. It also covers debugging MSBuild when invoked by VS and restoring the original state.\n\n## When to Use\n\n- You need Visual Studio's build (Ctrl+Shift+B, Build Solution, design-time builds) to use your local MSBuild changes.\n- You are debugging an issue that only reproduces when MSBuild is called through the VS process (`devenv.exe`).\n- You want to test MSBuild changes against a real VS solution end-to-end.\n\n**If you only need command-line testing**, prefer the bootstrap approach instead (see the `use-bootstrap-msbuild` skill). It is faster and does not risk breaking your VS installation.\n\n## Prerequisites\n\n- Windows (the deploy script targets VS on Windows).\n- The latest Visual Studio 2026 installed with the components listed in [`.vsconfig`](../../../.vsconfig).\n- .NET Framework 3.5 installed on the Windows machine.\n- An **administrator** PowerShell prompt (required to write into `Program Files`).\n- The MSBuild repo cloned and buildable.\n\n## Step 1: Build MSBuild\n\nRun a full build from the repo root to produce the bootstrap output:\n\n```powershell\n.\\build.cmd\n```\n\nBy default this builds `Debug` configuration. If you want `Release`:\n\n```powershell\n.\\build.cmd /p:Configuration=Release\n```\n\nThe build creates bootstrap binaries under `artifacts\\bin\\MSBuild.Bootstrap\\{configuration}\\`.\n\n## Step 2: Locate Your VS MSBuild Bin Folder\n\nFind the MSBuild `Bin` folder inside your VS installation. The path depends on your VS version and edition.\n\n**⚠ Important**: If you have multiple VS versions or editions installed side-by-side, make sure you identify the correct installation path for the instance you want to patch. Confirm the path exists before deploying.\n\nThe general pattern is:\n\n```text\nC:\\Program Files\\Microsoft Visual Studio\\{version}\\{edition}\\MSBuild\\Current\\Bin\n```\n\nFor Visual Studio 2026, `{version}` is `18` (not `2026`). Common `{edition}` folders include `Enterprise`, `Professional`, and `Community`; this is customizable, so use `vswhere` below to confirm the path.\n\nExample paths:\n\n| VS Version | Edition | Typical Path |\n| ---------- | ------- | ------------ |\n| 2026 | Enterprise | `C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin` |\n| 2026 | Community | `C:\\Program Files\\Microsoft Visual Studio\\18\\Community\\MSBuild\\Current\\Bin` |\n\n### Finding the path automatically\n\nThe most reliable way to locate your VS installation is with `vswhere`:\n\n```powershell\n# List all VS installations with their paths\n& \"${env:ProgramFiles(x86)}\\Microsoft Visual Studio\\Installer\\vswhere.exe\" -all -prerelease -format table\n\n# Get the MSBuild Bin path for the latest installation\n$vsPath = & \"${env:ProgramFiles(x86)}\\Microsoft Visual Studio\\Installer\\vswhere.exe\" -latest -prerelease -property installationPath\n\"$vsPath\\MSBuild\\Current\\Bin\"\n```\n\nAlternatively, from a VS Developer Command Prompt:\n\n```cmd\nwhere msbuild\n```\n\nUse the directory containing that `MSBuild.exe`.\n\n## Step 3: Deploy with the Script\n\nFrom an **administrator** PowerShell, run:\n\n```powershell\n.\\scripts\\Deploy-MSBuild.ps1 -destination \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\"\n```\n\n### Script Parameters\n\n| Parameter | Default | Description |\n| --------- | ------- | ----------- |\n| `-destination` | *(required)* | The VS MSBuild `Bin` folder path |\n| `-configuration` | `Debug` | Must match the configuration used in `build.cmd` |\n| `-runtime` | `Detect` | Auto-detects `Desktop` vs `Core` from the path. Use `Desktop` for VS, `Core` for .NET SDK |\n| `-binDirectory` | `artifacts\\bin` | Override if build output is elsewhere |\n| `-makeBackup` | `$true` | Creates a timestamped `Backup-*` folder before overwriting |\n\n### What Gets Copied\n\nThe script copies from `artifacts\\bin\\MSBuild.Bootstrap\\{configuration}\\net472\\`:\n\n- **Core DLLs**: `Microsoft.Build.dll`, `Microsoft.Build.Framework.dll`, `Microsoft.Build.Tasks.Core.dll`, `Microsoft.Build.Utilities.Core.dll`, `Microsoft.NET.StringTools.dll`\n- **Targets files**: `Microsoft.Common.targets`, `Microsoft.CSharp.targets`, `Microsoft.VisualBasic.targets`, and many more\n- **Executables**: `MSBuild.exe` (x86 and amd64), `MSBuildTaskHost.exe`\n- **Framework dependencies**: Various `System.*` assemblies needed for .NET Framework\n\n## Step 4: Test in Visual Studio\n\n1. **Close and reopen** Visual Studio (it caches MSBuild on startup).\n2. Open a solution and build — VS now uses your locally-built MSBuild.\n3. Verify by checking the build output or binary log for your changes.\n\n## Restoring the Original MSBuild\n\nThe deploy script creates a backup folder at `{destination}\\Backup-{timestamp}\\` by default.\n\nTo restore:\n\n```powershell\n# Find the backup folder\nGet-ChildItem \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\\Backup-*\"\n\n# Copy everything back\nCopy-Item \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\\Backup-{timestamp}\\*\" `\n          \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\\\" -Recurse -Force\n```\n\nAlternatively, run **Visual Studio Installer → Modify → Repair** to fully restore the original binaries.\n\n⚠ **CAUTION**: If you overwrite MSBuild in Visual Studio and something goes wrong, VS itself may break (since VS uses MSBuild internally). Always keep the backup and know how to restore before deploying.\n\n## Debugging MSBuild Inside Visual Studio\n\nWhen MSBuild is invoked by VS (for real builds or design-time builds), you can attach a debugger using environment variables. Set these **before** launching VS:\n\n### Breakpoint at BuildManager.BeginBuild (Recommended for VS)\n\n```powershell\n$env:MSBuildDebugBuildManagerOnStart = \"1\"   # 1 = Debugger.Launch(), 2 = wait for attach\n$env:MSBuildDebugProcessName = \"devenv\"      # Only trigger in the VS process\ndevenv.exe\n```\n\nThis breaks into `BuildManager.BeginBuild`, which is where VS calls into MSBuild's API.\n\n### Breakpoint at MSBuild Entry Point\n\n```powershell\n$env:MSBUILDDEBUGONSTART = \"1\"   # 1 = Launch debugger, 2 = Wait for attach, 3 = Main process only\ndevenv.exe\n```\n\n### Capturing Full Binary Logs from VS\n\nVS heavily filters MSBuild events and suppresses them entirely during design-time builds. To capture everything:\n\n```powershell\n$env:MSBuildDebugEngine = \"1\"\n$env:MSBUILDDEBUGPATH = \"C:\\temp\\MSBuild_Logs\"   # Optional: override log output directory\n$env:MSBuildDebugProcessName = \"devenv\"            # Optional: only log from VS\ndevenv.exe\n```\n\nThis injects a binary log at `BuildManager.BeginBuild`, capturing full logs for both real and design-time builds. Logs are saved to `MSBuild_Logs\\` under the current directory (or `MSBUILDDEBUGPATH` if set).\n\n## Deploying to .NET SDK Instead\n\nTo patch a .NET SDK installation rather than VS:\n\n```powershell\n.\\scripts\\Deploy-MSBuild.ps1 -destination \"C:\\Program Files\\dotnet\\sdk\\10.0.100\" -runtime Core\n```\n\nThe script auto-detects SDK paths (containing `dotnet` and `sdk`), but you can force it with `-runtime Core`.\n\n## Cross-Machine Deployment\n\nIf you cannot build MSBuild on the target machine:\n\n1. Build and deploy to an **empty folder**:\n\n   ```powershell\n   .\\scripts\\Deploy-MSBuild.ps1 -destination \"C:\\temp\\msbuild-deploy\"\n   ```\n\n2. Copy the contents of that folder to the target machine's VS MSBuild Bin folder.\n3. Make a manual backup of the target machine's files first.\n\n## Troubleshooting\n\n| Problem | Solution |\n| ------- | -------- |\n| Access denied when deploying | Run PowerShell as **Administrator** |\n| VS won't start after deploy | Restore from `Backup-*` folder or run VS Installer Repair |\n| Changes not reflected in VS | Close all VS instances and reopen; kill lingering `MSBuild.exe` processes |\n| Configuration mismatch | Ensure `-configuration` matches what you passed to `build.cmd` |\n| Build errors in MSBuild repo | Run `.\\build.cmd` from a clean state; see [Something's wrong in my build](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Something's-wrong-in-my-build.md) |\n\n## Quick Reference\n\n| Step | Command |\n| ---- | ------- |\n| Build MSBuild | `.\\build.cmd` |\n| Deploy to VS (admin) | `.\\scripts\\Deploy-MSBuild.ps1 -destination \"{VS MSBuild Bin path}\"` |\n| Deploy Release build | `.\\scripts\\Deploy-MSBuild.ps1 -destination \"{path}\" -configuration Release` |\n| Debug MSBuild in VS | Set `$env:MSBuildDebugBuildManagerOnStart = \"1\"` then launch `devenv.exe` |\n| Capture VS binlogs | Set `$env:MSBuildDebugEngine = \"1\"` then launch `devenv.exe` |\n| Restore original | Copy back from `Backup-*` folder or run VS Installer Repair |\n\n## See Also\n\n- [Deploy-MSBuild documentation](https://github.com/dotnet/msbuild/blob/main/documentation/Deploy-MSBuild.md)\n- [Building and Debugging on Full Framework](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md)\n- [Bootstrap Documentation](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Bootstrap.md)\n",".github/skills/dotnet-aot-compat/SKILL.md":"---\nname: dotnet-aot-compat\ndescription: >\n  Make .NET projects compatible with Native AOT and trimming by systematically\n  resolving IL trim/AOT analyzer warnings. USE FOR: making projects AOT-compatible,\n  fixing trimming warnings, resolving IL warnings (IL2026, IL2070, IL2067, IL2072,\n  IL3050), adding DynamicallyAccessedMembers annotations, enabling IsAotCompatible.\n  DO NOT USE FOR: publishing native AOT binaries, optimizing binary size, replacing\n  reflection-heavy libraries with alternatives.\n  INVOKES: no tools — pure knowledge skill.\nlicense: MIT\n---\n\n# dotnet-aot-compat\n\nMake .NET projects compatible with Native AOT and trimming by systematically resolving all IL trim/AOT analyzer warnings.\n\n## When to Use This Skill\n\n- **\"Make this project AOT-compatible\"**\n- **\"Fix trimming warnings\"** or **\"fix IL warnings\"**\n- **\"Resolve IL2070 / IL2067 / IL2072 / IL2026 / IL3050 warnings\"**\n- **\"Add DynamicallyAccessedMembers annotations\"**\n- **\"Enable IsAotCompatible in my .csproj\"**\n- **\"My project has trim analyzer warnings after upgrading to net8.0\"**\n- **\"Annotate reflection code for the trimmer\"**\n\n## When Not to Use This Skill\n\nDo not use this skill when the project exclusively targets .NET Framework (net4x), which does not support the trim/AOT analyzers.\n\n## Prerequisites\n\nAn existing .NET project targeting net8.0 or later (or multi-targeting with at least one net8.0+ TFM) and the corresponding .NET SDK installed.\n\n## Background: What AOT Compatibility Means\n\nNative AOT and the IL trimmer perform static analysis to determine what code is reachable. Reflection can break this analysis because the trimmer can't see what types/members are accessed at runtime. The `IsAotCompatible` property enables analyzers that flag these issues as build warnings (ILXXXX codes).\n\n## Critical Rules\n\n### ❌ Never suppress warnings incorrectly\n\n- **NEVER** use `#pragma warning disable` for IL warnings. It hides warnings from the Roslyn analyzer at build time, but the IL linker and AOT compiler still see the issue. The code will fail at trim/publish time.\n- **NEVER** use `[UnconditionalSuppressMessage]`. It tells both the analyzer AND the linker to ignore the warning, meaning the trimmer cannot verify safety. Raising an error at build time is always preferable to hiding the issue and having it silently break at runtime.\n\n### 💡 Preferred approaches\n\n- **Prefer** `[DynamicallyAccessedMembers]` annotations to flow type information through the call chain.\n- **Prefer** refactoring to eliminate patterns that break annotation flow (e.g., boxing `Type` through `object[]`).\n- **Use** `[RequiresUnreferencedCode]` / `[RequiresDynamicCode]` / `[RequiresAssemblyFiles]` to mark methods as fundamentally incompatible with trimming, propagating the requirement to callers. This surfaces the issue clearly rather than hiding it — callers must explicitly acknowledge the incompatibility.\n\n### Annotation flow is key\n\nThe trimmer tracks `[DynamicallyAccessedMembers]` annotations through assignments, parameter passing, and return values. If this flow is broken (e.g., by boxing a `Type` into `object`, storing in an untyped collection, or casting through interfaces), the trimmer loses track and warns. The fix is to preserve the flow, not suppress the warning.\n\n## Step-by-Step Procedure\n\n> **Do not explore the codebase up-front.** The build warnings tell you exactly which files and lines need changes. Follow a tight loop: **build → pick a warning → open that file at that line → apply the fix recipe → rebuild**. Reading or analyzing source files beyond what a specific warning points you to is wasted effort and leads to timeouts. Let the compiler guide you.\n>\n> ❌ Do NOT run `find`, `ls`, or `grep` to understand the project structure before building. Do NOT read README, docs, or architecture files. Your first action should be Step 1 (enable AOT analysis), then build.\n\n### Step 1: Enable AOT analysis in the .csproj\n\nAdd `IsAotCompatible`. If the project doesn't exclusively target net8.0+, add a TFM condition (AOT analysis requires net8.0+):\n\n```xml\n<PropertyGroup>\n  <IsAotCompatible Condition=\"$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))\">true</IsAotCompatible>\n</PropertyGroup>\n```\n\nThis automatically sets `EnableTrimAnalyzer=true` and `EnableAotAnalyzer=true` for compatible TFMs. For multi-targeting projects (e.g., `netstandard2.0;net8.0`), the condition ensures no `NETSDK1210` warnings on older TFMs.\n\n### Step 2: Build and collect warnings\n\n```bash\ndotnet build <project.csproj> -f <net8.0-or-later-tfm> --no-incremental 2>&1 | grep 'IL[0-9]\\{4\\}'\n```\n\nSort and deduplicate. Common warning codes:\n- **IL2070**: Reflection call on a `Type` parameter missing `[DynamicallyAccessedMembers]`\n- **IL2067**: Passing an unannotated `Type` to a method expecting `[DynamicallyAccessedMembers]`\n- **IL2072**: Return value or extracted value missing annotation (often from unboxing)\n- **IL2057**: `Type.GetType(string)` with a non-constant argument\n- **IL2026**: Calling a method marked `[RequiresUnreferencedCode]`\n- **IL2050**: P/invoke method with COM marshalling parameters\n- **IL2075**: Return value flows into reflection without annotation\n- **IL2091**: Generic argument missing `[DynamicallyAccessedMembers]` required by constraint\n- **IL3000**: `Assembly.Location` returns empty string in single-file/AOT apps\n- **IL3050**: Calling a method marked `[RequiresDynamicCode]`\n\n### Step 3: Triage warnings by code (do NOT read every file)\n\nGroup the warnings from Step 2 by warning code and count them. **Do not open individual files yet.** Identify the top 1-2 patterns by count — these drive your fix strategy:\n\n| Pattern | Typical fix |\n|---------|-------------|\n| Many IL2026 + IL3050 from `JsonSerializer` | **Go to Strategy C immediately** — create a `JsonSerializerContext`, then batch-update all call sites |\n| IL2070/IL2087 on `Type` parameters | Add `[DynamicallyAccessedMembers]` to the innermost method, then cascade outward |\n| IL2067 passing unannotated `Type` | Annotate the parameter at the source |\n\n**In most real projects, IL2026/IL3050 from JsonSerializer dominate.** Start with Strategy C unless the warning breakdown clearly shows otherwise. After the batch JSON fix, handle remaining warnings with Strategies A–B. Only use Strategy D as a last resort.\n\n### Step 4: Fix warnings iteratively (innermost first)\n\nWork from the **innermost** reflection call outward. Each fix may cascade new warnings to callers.\n\n**Stay warning-driven.** For each warning, open only the file and line the compiler reported, identify the pattern, apply the matching fix recipe below, and move on. Do not scan the codebase for similar patterns or try to understand the full architecture — fix what the compiler tells you, rebuild, and let new warnings guide the next change. Fix a small batch of warnings (5-10), then rebuild immediately to check progress.\n\n**Use sub-agents when available.** If you can launch sub-agents (e.g., via a `task` tool), dispatch **multiple sub-agents in parallel** to edit different files simultaneously. Keep the main loop focused on building, parsing warnings, and dispatching — delegate actual file edits to sub-agents. For batch JSON updates, give each sub-agent 5-10 files to update in one prompt. **After 2 build-fix cycles, dispatch all remaining file edits to sub-agents in parallel — do not continue fixing files sequentially.** Example:\n\n> Update these files to use source-generated JSON: `src/Models/Resource.Serialization.cs`, `src/Models/Identity.Serialization.cs`, `src/Models/Plan.Serialization.cs`. In each file, replace `JsonSerializer.Serialize(writer, value)` with `JsonSerializer.Serialize(writer, value, MyProjectJsonContext.Default.TypeName)` and `JsonSerializer.Deserialize<T>(ref reader)` with `JsonSerializer.Deserialize(ref reader, MyProjectJsonContext.Default.TypeName)`. Only edit the JsonSerializer call sites.\n\n#### Strategy A: Add `[DynamicallyAccessedMembers]` (preferred)\n\nWhen a method uses reflection on a `Type` parameter, annotate the parameter to tell the trimmer what members are needed:\n\n```csharp\nusing System.Diagnostics.CodeAnalysis;\n\n// Before (warns IL2070):\nvoid Process(Type t) {\n    var method = t.GetMethod(\"Foo\");  // trimmer can't verify\n}\n\n// After (clean):\nvoid Process([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type t) {\n    var method = t.GetMethod(\"Foo\");  // trimmer preserves public methods\n}\n```\n\nWhen you annotate a parameter, **all callers** must now pass properly annotated types. This cascades outward — follow each caller and annotate or refactor as needed. **The caller's annotation must include at least the same member types as the callee's.** If the callee requires `PublicConstructors | NonPublicConstructors`, the caller must specify the same or a superset — using only `NonPublicConstructors` will produce IL2091.\n\n#### Strategy B: Refactor to preserve annotation flow\n\nWhen annotation flow is broken by boxing (storing `Type` in `object`, `object[]`, or untyped collections), **refactor** to pass the `Type` directly:\n\n```csharp\n// BROKEN: Type boxed into object[], annotation lost\nvoid Process(object[] args) {\n    Type t = (Type)args[0];  // IL2072: annotation lost through boxing\n    Evaluate(t, ...);\n}\n\n// FIXED: Pass Type as a separate, annotated parameter\nvoid Process(\n    object[] args,\n    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type calleeType,\n    ...) {\n    Evaluate(calleeType, ...);  // annotation flows cleanly\n}\n```\n\nCommon patterns that break flow and how to fix them:\n- **`object[]` parameter bags**: Extract the `Type` into a dedicated annotated parameter\n- **Dictionary/List storage**: Use a typed field with annotation instead\n- **Interface indirection**: Add annotation to the interface method's parameter\n- **Property with boxing getter**: Annotate the property's return type\n\n#### Strategy C: Source-generated JSON serialization (batch fix)\n\nWhen most warnings are IL2026/IL3050 from `JsonSerializer.Serialize`/`Deserialize`, this is a single mechanical fix applied in bulk:\n\n1. **Collect affected types** — grep for all `JsonSerializer.Serialize` and `JsonSerializer.Deserialize` call sites. Extract the type being serialized (the `<T>` in `Deserialize<T>`, or the runtime type of the object in `Serialize`).\n\n2. **Create one `JsonSerializerContext`** with `[JsonSerializable]` for every type found. **Skip types from external packages** (e.g., `ResponseError` from `Azure.Core`) — they won't source-generate for types you don't own. Handle external types separately via Gotcha #1 below.\n\n```csharp\n[JsonSerializerContext]\n[JsonSerializable(typeof(ManagedServiceIdentity))]\n[JsonSerializable(typeof(SystemData))]\n// ... one attribute per type YOU OWN\n// Do NOT add types from external packages (e.g., ResponseError)\ninternal partial class MyProjectJsonContext : JsonSerializerContext { }\n```\n\n3. **Batch-update all call sites** — do not read each file individually. Apply the pattern mechanically:\n   - `JsonSerializer.Serialize(obj)` → `JsonSerializer.Serialize(obj, MyProjectJsonContext.Default.TypeName)`\n   - `JsonSerializer.Deserialize<T>(json)` → `JsonSerializer.Deserialize(json, MyProjectJsonContext.Default.TypeName)`\n\n   Find and update all call sites in one pass:\n   ```bash\n   # Find all files with JsonSerializer calls\n   grep -rl 'JsonSerializer\\.\\(Serialize\\|Deserialize\\)' src/ --include='*.cs'\n   ```\n   Then use sequential `edit` calls to apply the same transformation to every matching file. **Do not use `sed` for C# code** — generics like `Deserialize<T>()` have angle brackets and nested parentheses that sed will mangle.\n\n4. **Build once** to verify. Remaining warnings will be non-serialization issues — handle those with Strategies A–B or D.\n\n#### Strategy D: `[RequiresUnreferencedCode]` (last resort)\n\nWhen a method fundamentally requires arbitrary reflection that cannot be statically described:\n\n```csharp\n[RequiresUnreferencedCode(\"Loads plugins by name using Assembly.Load\")]\npublic void LoadPlugin(string assemblyName) {\n    var asm = Assembly.Load(assemblyName);\n    // ...\n}\n```\n\nThis propagates to callers — they must also be annotated with `[RequiresUnreferencedCode]`. Use sparingly; it marks the entire call chain as trim-incompatible.\n\n### Step 5: Rebuild and repeat\n\nAfter each small batch of fixes (5-10 warnings), rebuild with `--no-incremental` and check for new warnings. **Do not attempt to fix all warnings before rebuilding** — frequent rebuilds catch mistakes early and reveal cascading warnings. Fixes cascade — annotating an inner method may surface warnings in its callers. Repeat until `0 Warning(s)`.\n\n### Step 6: Validate all TFMs\n\nBuild all target frameworks to ensure:\n- **0 IL warnings** on net8.0+ TFMs\n- **No NETSDK1210 warnings** (the `IsAotCompatible` condition handles this)\n- **Clean builds** on older TFMs (netstandard2.0, net472, etc.)\n\n```bash\ndotnet build <project.csproj>  # builds all TFMs\n```\n\n## Stop Signals\n\n- **Do not analyze more than 2-3 representative files per warning pattern.** After identifying the fix for a pattern, apply it to all matching files without reading each one first.\n- **Start fixing after one build.** Do not do a second analysis pass — begin implementing fixes for the most common warning pattern immediately after Step 3 triage.\n- Stop after achieving **0 IL warnings** for net8.0+ TFMs. Don't optimize or refactor already-clean annotations.\n- If a warning requires **architectural refactoring** beyond annotation flow fixes (e.g., replacing an entire serialization layer), document it and stop — don't rewrite large subsystems.\n- Limit to **3 build-fix iterations** per warning. If annotation flow doesn't resolve it after 3 attempts, escalate to `[RequiresUnreferencedCode]`.\n- Don't chase warnings in **third-party dependencies** you can't modify. Note them and move on.\n- If the user asked a scoped question (e.g., \"fix warnings in this file\"), don't expand to the entire project.\n\n## Polyfills for Older TFMs\n\nFor multi-targeting projects that include netstandard2.0 or net472, you need polyfills for `DynamicallyAccessedMembersAttribute` and related types. See [references/polyfills.md](references/polyfills.md).\n\n## Common Gotchas\n\n1. **External types without AOT-safe serialization**: When a type comes from a dependency you can't modify (e.g., `ResponseError` from `Azure.Core`) and it lacks a source-generated serializer, `Options.GetConverter<T>()` is reflection-based and will produce IL warnings. First check if the type implements `IJsonModel<T>` (common in Azure SDK) — if so, bypass `JsonSerializer` entirely:\n\n```csharp\n// Before (IL2026 — JsonSerializer uses reflection):\nJsonSerializer.Serialize(writer, errorValue);\n\n// After (AOT-safe — uses IJsonModel directly):\n((IJsonModel<ResponseError>)errorValue).Write(writer, ModelReaderWriterOptions.Json);\n\n// For deserialization:\nvar error = ((IJsonModel<ResponseError>)new ResponseError()).Create(ref reader, ModelReaderWriterOptions.Json);\n```\n\nDo **not** add the external type to your `JsonSerializerContext` — it won't source-generate for types you don't own. If the type doesn't implement `IJsonModel<T>`, write a custom `JsonConverter<T>` with manual `Utf8JsonReader`/`Utf8JsonWriter` logic and register it via `[JsonSourceGenerationOptions]` on your context.\n\n2. **Serialization libraries**: Most reflection-based serializers (e.g., `Newtonsoft.Json`, `XmlSerializer`) are not AOT-compatible. Migrate to a source-generation-based serializer such as `System.Text.Json` with a `JsonSerializerContext`. If migration is not feasible, mark the serialization call site with `[RequiresUnreferencedCode]`.\n\n3. **Shared projects / projitems**: When source is shared between multiple projects via `<Import>`, annotations added to shared code affect ALL consuming projects. Verify that all consumers still build cleanly.\n\n## References\n\n[Limitations](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=windows%2Cnet8#limitations-of-native-aot-deployment)\n[Conceptual: Understanding trimming](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-concepts)\n[How-to: trim compat](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/fixing-warnings)\n\n## Checklist\n\n- [ ] Added `<IsAotCompatible>` with TFM condition to .csproj\n- [ ] Built with AOT analyzers enabled (net8.0+ TFM)\n- [ ] Fixed all IL warnings via annotations or refactoring\n- [ ] No `#pragma warning disable` or `[UnconditionalSuppressMessage]` used for any IL warning\n- [ ] Polyfills present for older TFMs if needed\n- [ ] All target frameworks build with 0 warnings\n- [ ] Verified shared/linked source doesn't break sibling projects\n",".github/skills/dotnet-aot-compat/references/polyfills.md":"# Polyfills for Older TFMs\n\n`DynamicallyAccessedMembersAttribute` shipped in .NET 5. For projects targeting netstandard2.0 or net472, you need a polyfill. The trimmer recognizes the attribute by name, so a local copy works:\n\n```csharp\n#if !NET\nnamespace System.Diagnostics.CodeAnalysis\n{\n    [AttributeUsage(AttributeTargets.Field | AttributeTargets.ReturnValue |\n                    AttributeTargets.GenericParameter | AttributeTargets.Parameter |\n                    AttributeTargets.Property, Inherited = false)]\n    internal sealed class DynamicallyAccessedMembersAttribute : Attribute\n    {\n        public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes)\n            => MemberTypes = memberTypes;\n        public DynamicallyAccessedMemberTypes MemberTypes { get; }\n    }\n\n    [Flags]\n    internal enum DynamicallyAccessedMemberTypes\n    {\n        None = 0,\n        PublicParameterlessConstructor = 0x0001,\n        PublicConstructors = 0x0002 | PublicParameterlessConstructor,\n        NonPublicConstructors = 0x0004,\n        PublicMethods = 0x0008,\n        NonPublicMethods = 0x0010,\n        PublicFields = 0x0020,\n        NonPublicFields = 0x0040,\n        PublicNestedTypes = 0x0080,\n        NonPublicNestedTypes = 0x0100,\n        PublicProperties = 0x0200,\n        NonPublicProperties = 0x0400,\n        PublicEvents = 0x0800,\n        NonPublicEvents = 0x1000,\n        Interfaces = 0x2000,\n        All = ~None // Discouraged — prefer specific flags\n    }\n}\n#endif\n```\n\nSimilarly for `RequiresUnreferencedCodeAttribute` and `UnconditionalSuppressMessageAttribute` if needed on older TFMs.\n",".github/skills/integrating-sdk-and-msbuild/SKILL.md":"---\nname: integrating-sdk-and-msbuild\ndescription: 'Guides work on the SDK-MSBuild integration boundary. Consult when authoring or modifying SDK targets, working on dotnet CLI to MSBuild invocation, handling project-reference protocol, coordinating cross-repo changes with dotnet/sdk, debugging property resolution or import ordering, working on restore/build/publish/pack target chains, or dealing with Directory.Build.props/targets interaction.'\nargument-hint: 'Describe the SDK integration scenario or cross-repo coordination need.'\n---\n\n# SDK-MSBuild Integration Patterns\n\nMSBuild operates as a component within the .NET SDK. This boundary is the most complex integration point in the .NET build stack, spanning MSBuild (engine), SDK (target implementations), NuGet (restore), and Roslyn (compilation).\n\n## The Evaluation Boundary\n\nUnderstanding MSBuild's evaluation order is critical for SDK target authoring:\n\n```\n1. Environment variables\n2. Global properties (from CLI: -p:Foo=Bar)\n3. Project-level properties (file order, with imports):\n   ┌─ Sdk.props (SDK defaults)\n   ├─ Directory.Build.props (user overrides BEFORE project)\n   ├─ <Project> properties (the .csproj itself)\n   ├─ Directory.Build.targets (user overrides AFTER project)\n   └─ Sdk.targets (SDK target definitions)\n4. Item definitions\n5. Items (including SDK default globs)\n```\n\n### Key Import Order Rules\n\n- **SDK props import BEFORE user project** — SDK defaults can be overridden by the user\n- **SDK targets import AFTER user project** — SDK targets see user-specified properties\n- **`Directory.Build.props` is imported from `Microsoft.Common.props` as an early user extension point after core defaults are computed** — use it for solution-wide customization\n- **Property defaults set in SDK must not override user-specified values** — always use `Condition=\"'$(Prop)' == ''\"`\n\n```xml\n<!-- CORRECT: SDK default that respects user override -->\n<OutputType Condition=\"'$(OutputType)' == ''\">Library</OutputType>\n\n<!-- WRONG: Unconditional set clobbers user's .csproj -->\n<OutputType>Library</OutputType>\n```\n\n## Restore and Build Separation\n\n**Restore and Build must never run in the same evaluation.** The restore phase generates `.g.props` and `.g.targets` files that must be imported during evaluation — but they don't exist until restore completes.\n\n- `dotnet build` implicitly runs restore then build as **separate invocations**\n- `dotnet build --no-restore` skips restore, assuming it already happened\n- Running both targets in one invocation (`/t:Restore;Build`) is a known anti-pattern that causes intermittent failures\n\n## Project Reference Protocol\n\nThe project-reference protocol spans MSBuild, SDK, and NuGet. It is the most complex integration boundary.\n\n### How It Works\n\n1. Outer build dispatches to `_GetProjectReferenceTargetFrameworkProperties` to determine inner build parameters\n2. Inner build runs with the resolved `TargetFramework` (singular) for each referenced project\n3. `GetTargetPath` returns the output assembly for the referencing project to consume\n\n### Rules\n\n- Protocol changes must be coordinated across MSBuild, SDK, and NuGet teams\n- Multi-targeting projects (`<TargetFrameworks>`) dispatch multiple inner builds\n- The outer build must not assume a single target framework\n- `SetTargetFramework` is how the outer build communicates the chosen framework to inner builds\n\n## Target Authoring in SDK Context\n\n### Extension Points\n\nSDK provides well-known extension points for targets:\n\n| Extension Point | Use For |\n|----------------|---------|\n| `$(BuildDependsOn)` | Adding to the Build chain |\n| `$(CompileDependsOn)` | Pre-compilation steps |\n| `$(PublishDependsOn)` | Publish pipeline additions |\n| `$(PackDependsOn)` | NuGet pack pipeline additions |\n| `BeforeTargets=\"Build\"` | Use sparingly; prefer `DependsOnTargets` |\n\n### Ordering Rules\n\n1. **Use `DependsOnTargets` for required predecessors** — it's explicit and predictable\n2. **`BeforeTargets`/`AfterTargets` should be used sparingly** — they create implicit ordering that's hard to debug\n3. **Incremental build targets need precise `Inputs` and `Outputs`** — incorrect declarations cause either rebuild-every-time or stale-output bugs\n4. **Test with multi-targeting** — target chains execute once per `TargetFramework` in the inner build\n\n## Cross-Repo Coordination\n\nChanges that touch the MSBuild-SDK boundary often require coordinated PRs:\n\n1. **MSBuild engine change** → may need SDK target updates\n2. **SDK target change** → may need MSBuild API additions\n3. **NuGet restore change** → affects both MSBuild evaluation and SDK targets\n\n### Coordination Protocol\n\n- File an issue in both repos describing the cross-cutting change\n- Land the MSBuild change first (lower in the stack)\n- Update SDK to consume the new MSBuild via dependency flow\n- Test end-to-end with the SDK's MSBuild integration tests\n\n## Design-Time Builds\n\nVisual Studio uses design-time builds with different target contracts:\n\n- Design-time builds call `ResolveProjectReferences` but not `Build`\n- They set `$(DesignTimeBuild)=true` and `$(BuildingProject)=false`\n- Targets that should not run during design-time must check these properties\n- Design-time builds must be fast — avoid expensive I/O or compilation\n\n## Common Integration Bugs\n\n| Symptom | Likely Cause |\n|---------|-------------|\n| Property has wrong value | Import ordering — check if SDK prop overrides user setting |\n| Target runs in wrong order | Missing `DependsOnTargets` declaration |\n| Build works, restore fails | Evaluation-time dependency on restore-generated files |\n| Works single-target, fails multi-target | Target assumes single `$(TargetFramework)` |\n| CLI build works, VS build fails | Design-time build target contract violation |\n",".github/skills/maintaining-binary-log-compatibility/SKILL.md":"---\nname: maintaining-binary-log-compatibility\ndescription: 'Guides changes to MSBuild binary log infrastructure. Consult when modifying BinaryLogger or BinaryLogReplayEventSource, adding new BuildEventArgs types, changing event serialization/deserialization, modifying ProjectImportsCollector, adjusting message importance levels, or making changes that affect .binlog content. Also applies when verifying that behavioral changes are properly reflected in binary log output.'\nargument-hint: 'Describe the binary log change or event serialization concern.'\n---\n\n# Binary Log Considerations\n\nThe binary log (`.binlog`) is MSBuild's primary diagnostic format and source of truth for build analysis. It captures the complete build event stream with full fidelity. Format changes have strict compatibility requirements.\n\nFor general binary log usage, see [Binary-Log.md](../../../documentation/wiki/Binary-Log.md).\n\n## Core Principles\n\n1. **The binlog captures everything.** All meaningful build events must be represented. If your change alters build behavior, it must be observable in the binlog.\n2. **Format changes must be backward-compatible.** Older versions of MSBuild Structured Log Viewer and other tools must be able to read binlogs produced by newer MSBuild, at minimum gracefully degrading.\n3. **Forward compatibility matters too.** Newer viewers should handle binlogs from older MSBuild without crashing, even if some events are unrecognized.\n\n## Architecture Overview\n\n```\nBuild Engine\n  → BuildEventArgs (structured events)\n    → BinaryLogger (serializes to .binlog)\n      → ProjectImportsCollector (embeds project/targets files)\n\nReplay:\n  .binlog file\n    → BinaryLogReplayEventSource (deserializes)\n      → Any ILogger (console, structured log viewer, analyzers)\n```\n\nKey source files:\n- `src/Build/Logging/BinaryLogger/BinaryLogger.cs` — the logger itself\n- `src/Framework/BuildEventArgs.cs` — base class for all build events\n- `src/Build/Logging/BinaryLogger/ProjectImportsCollector.cs` — captures imported files\n\n## Adding New Build Event Types\n\nWhen adding a new `BuildEventArgs` subclass:\n\n1. **Define the new event class** inheriting from the appropriate base (`BuildMessageEventArgs`, `BuildWarningEventArgs`, etc.)\n2. **Add serialization and forwarding support** — implement `WriteToStream` and `CreateFromStream`, then trace the event through every dispatcher and transport (including task-host forwarding) without making unknown event types fatal\n3. **Increment the binary log version** if the new event type changes the format\n4. **Add a new record type constant** in the binary logger's record type enum\n5. **Handle the unknown-type case** in the replay source — older readers must skip gracefully\n\n### Serialization Compatibility Rules\n\n- **Never remove fields** from existing event args serialization\n- **New fields must be appended** to the end of the serialization stream\n- **Use version checks** when reading — if the binlog version is older, use defaults for new fields\n- **Nullable fields** should serialize a presence flag before the value\n\n```csharp\n// Pattern for backward-compatible field addition\nif (logVersion >= newFieldVersion)\n{\n    writer.Write(newField);\n}\n\n// Reading with backward compatibility\nif (logVersion >= newFieldVersion)\n{\n    newField = reader.ReadString();\n}\nelse\n{\n    newField = defaultValue;\n}\n```\n\n## Message Importance Levels\n\nImportance controls what appears in console output, but **everything goes to binlog** regardless of importance.\n\n| Level | Use For | Console Verbosity |\n|-------|---------|------------------|\n| `High` | Critical user-facing information | Minimal and above |\n| `Normal` | Standard build progress | Normal and above |\n| `Low` | Detailed diagnostic information | Detailed and above |\n| `Diagnostic` | Internal debugging | Diagnostic only |\n\n### Rules\n\n- Default to `Normal` for user-relevant information\n- Use `Low` for information useful when debugging but noisy in normal builds\n- `High` is reserved for important warnings/status — use sparingly\n- Never skip logging because \"it's too verbose\" — log at `Low` instead\n\n## ProjectImportsCollector\n\nThe `ProjectImportsCollector` embeds all imported `.props`, `.targets`, and project files into the binlog. This enables the \"preprocessed view\" in log viewers.\n\n### Considerations\n\n- `MSBUILDLOGIMPORTS=1` (or the `/bl` switch) enables import collection\n- Imported file content is captured at evaluation time — reflects the actual content used\n- Large import chains increase binlog size — this is acceptable for diagnostic completeness\n- Sensitive content in imported files will be embedded — document this for users\n\n## Changes That Affect Binlog Content\n\nWhen modifying MSBuild behavior, verify binlog impact:\n\n| Change Type | Binlog Consideration |\n|------------|---------------------|\n| New property set during evaluation | Appears in `PropertyInitialValue` or `PropertyReassignment` events |\n| New target added | Produces `TargetStarted`/`TargetFinished` events |\n| Changed task behavior | Task output items/properties captured in `TaskFinished` |\n| New warning/error | Captured as `BuildWarningEventArgs`/`BuildErrorEventArgs` |\n| Modified import chain | Changes which files `ProjectImportsCollector` captures |\n\n## Testing Binlog Changes\n\n- **Round-trip test**: Write a binlog, replay it, verify all events are reconstructed\n- **Version compatibility test**: Verify older replay sources handle new events gracefully\n- **Content verification**: Assert specific events appear in the binlog for behavioral changes\n- **Size regression**: Monitor binlog size for unexpectedly large increases\n- Use `BinaryLogReplayEventSource` in tests to verify binlog content programmatically\n\n## Checklist\n\n- [ ] New event types have `WriteToStream`/`CreateFromStream` implementations\n- [ ] Binary log format version incremented if format changed\n- [ ] Backward compatibility: older readers skip/degrade gracefully\n- [ ] Forward compatibility: newer readers handle old format\n- [ ] Importance levels set correctly for new messages\n- [ ] Behavioral changes produce observable binlog events\n- [ ] Round-trip test passes (serialize → deserialize → verify)\n",".github/skills/merge-dependency-updates/SKILL.md":"---\nname: merge-dependency-updates\ndescription: 'Review and merge open bot PRs: dependency updates from dotnet-maestro, codeflow from dotnet/dotnet, and OneLoc localization PRs. Produces a clickable dashboard with CI status, review state, and suspicious file flags. Use when you want to triage all open bot PRs in one pass.'\nargument-hint: 'Triage, update, and version-bump open bot PRs.'\n---\n\n# Managing Bot PRs (Dependencies, Codeflow, OneLoc)\n\nThis skill triages all open bot PRs in the MSBuild repo, produces a review dashboard, then optionally updates branches and bumps versions for merge readiness.\n\n## When to Use\n\n- You want to see the status of all open bot PRs at a glance\n- There are open dependency update / codeflow / OneLoc PRs that need merging\n- You want to process all of them in one pass with a clickable summary\n\n## PR Categories\n\n| Category | Author | Title Pattern | Example |\n|----------|--------|---------------|---------|\n| Dependency update | `dotnet-maestro[bot]` | `[<branch>] Update dependencies from dotnet/<repo>` | `[vs18.3] Update dependencies from dotnet/arcade` |\n| Codeflow | `dotnet-maestro[bot]` | `[<branch>] Source code updates from dotnet/dotnet` | `[vs18.3] Source code updates from dotnet/dotnet` |\n| OneLoc | `dotnet-bot` | `Localized file check-in by OneLocBuild Task: ...` | `Localized file check-in by OneLocBuild Task: Build definition ID 9434: Build ID ...` |\n\n## Step-by-Step Procedure\n\n### Step 1: Gather All Bot PRs\n\nRun **two searches in parallel** to find all open bot PRs:\n\n1. **Dependency updates + Codeflow:**\n   ```\n   Search: is:open author:app/dotnet-maestro repo:dotnet/msbuild\n   ```\n\n2. **OneLoc localization:**\n   ```\n   Search: is:open author:dotnet-bot repo:dotnet/msbuild (\"OneLocBuild\" OR \"Localized file check-in\")\n   ```\n\n### Step 2: Analyze Each PR (in parallel)\n\nFor each PR, query the following data using `gh api graphql`. Process PRs in parallel batches for speed.\n\n```bash\ngh api graphql -f query='\n  query {\n    repository(owner: \"dotnet\", name: \"msbuild\") {\n      pullRequest(number: <PR_NUMBER>) {\n        id\n        title\n        baseRefName\n        headRefName\n        mergeable\n        reviewDecision\n        commits(last: 1) {\n          nodes {\n            commit {\n              statusCheckRollup {\n                state\n              }\n            }\n          }\n        }\n        reviews(last: 10) {\n          nodes {\n            state\n            author { login }\n          }\n        }\n        reviewThreads(first: 20) {\n          nodes {\n            isResolved\n            comments(first: 1) {\n              nodes { body author { login } }\n            }\n          }\n        }\n        files(first: 50) {\n          nodes { path additions deletions }\n          totalCount\n        }\n      }\n    }\n  }\n'\n```\n\nFrom the response, extract:\n- **CI status**: from `statusCheckRollup.state` — `SUCCESS`, `FAILURE`, `PENDING`, `ERROR`, `EXPECTED`, or `null`. Map `ERROR` to failure (❌) and `EXPECTED` to neutral (⚪) in the dashboard.\n- **Review decision**: `APPROVED`, `REVIEW_REQUIRED`, `CHANGES_REQUESTED`\n- **Approvals**: count and list of `APPROVED` reviews\n- **Review comments**: any unresolved threads from the first 20 results (especially `CHANGES_REQUESTED`)\n- **Files changed**: use `totalCount` for the file count; use the first 50 file paths from `files.nodes` for suspicious file detection\n- **Suspicious flag**: check if any file paths are outside expected patterns (see below)\n\n#### Suspicious File Detection\n\n| PR Category | Expected Files | Flag if... |\n|-------------|---------------|------------|\n| Dependency update (arcade) | `eng/**`, `global.json` | Any file outside `eng/` or `global.json` |\n| Dependency update (roslyn/nuget/runtime) | `eng/Version.Details.xml`, `eng/Version.Details.props` | Any file outside `eng/` |\n| Codeflow (dotnet/dotnet) | `eng/Version.Details.xml` | Any `src/` files modified |\n| OneLoc | `**/xlf/*.xlf`, `**/Resources/*.resx` | Any non-localization file, or reviewers flagging reverted translations |\n\n### Step 3: Update Branches & Bump Versions (dependency PRs only)\n\nFor each **dependency update** PR:\n\n#### 3a. Update the PR branch\n\nUse the GitHub GraphQL `updatePullRequestBranch` mutation — the exact equivalent of the \"Update branch\" button:\n\n```bash\ngh api graphql -f query='\n  mutation {\n    updatePullRequestBranch(input: {pullRequestId: \"<NODE_ID>\"}) {\n      pullRequest { number headRefOid }\n    }\n  }\n'\n```\n\n#### 3b. Bump VersionPrefix (vs < 18.10 branches only)\n\n**Skip for `vs18.10` and newer vs* branches** - they don't need to increment the patch version with every dependency update anymore. Only bump for `vs*` branches that are older than `vs18.10`.\n\n1. Read `<VersionPrefix>X.Y.Z</VersionPrefix>` from the **target branch's** `eng/Versions.props`\n2. Increment patch: `X.Y.Z` → `X.Y.(Z+1)`\n3. Checkout the PR branch, update the version, commit, push:\n   ```bash\n   git fetch origin <head-branch>\n   git checkout <head-branch>\n   # Edit eng/Versions.props: set <VersionPrefix> to X.Y.(Z+1)\n   git add eng/Versions.props\n   git commit -m \"Bump VersionPrefix to X.Y.(Z+1)\"\n   git push origin <head-branch>\n   ```\n\n**Note:** The `<VersionPrefix>` line may be inline with `<DotNetFinalVersionKind>` — preserve that formatting.\n\n### Step 4: Produce the Dashboard\n\nOutput a **single dashboard** with all PRs grouped by category. This is the most important output — it lets the user click through and approve/reject each PR.\n\nFormat:\n\n```\n## 🚦 Dependency Update PRs\n\n| PR | Branch | CI | Review | Files | Suspicious? | Link |\n|----|--------|-----|--------|-------|-------------|------|\n| #13281 | main ← roslyn | ✅ SUCCESS | ✅ APPROVED (1) | 2 | ✅ Clean | https://github.com/dotnet/msbuild/pull/13281 |\n| #13310 | vs17.10 ← arcade | ❌ FAILURE | 🔶 REVIEW_REQUIRED | 14 | ✅ Clean (eng/ only) | https://github.com/dotnet/msbuild/pull/13310 |\n\n## 🔄 Codeflow PRs\n\n| PR | Branch | CI | Review | Files | Suspicious? | Link |\n|----|--------|-----|--------|-------|-------------|------|\n| #13258 | vs18.3 ← dotnet/dotnet | ✅ SUCCESS | 🔶 REVIEW_REQUIRED | 1 | ✅ Clean | https://github.com/dotnet/msbuild/pull/13258 |\n\n## 🌐 OneLoc PRs\n\n| PR | Branch | CI | Review | Files | Suspicious? | Link |\n|----|--------|-----|--------|-------|-------------|------|\n| #13290 | main | ⚪ None | ⛔ CHANGES_REQUESTED | 65 | ⚠️ Reverts translations | https://github.com/dotnet/msbuild/pull/13290 |\n```\n\nStatus icons:\n- CI: ✅ SUCCESS, ❌ FAILURE/ERROR, ⏳ PENDING, ⚪ None/EXPECTED\n- Review: ✅ APPROVED, 🔶 REVIEW_REQUIRED, ⛔ CHANGES_REQUESTED\n- Suspicious: ✅ Clean, ⚠️ with description\n\nAfter the tables, add a summary:\n> **N PRs total. M ready to merge, K need reviews, J have CI failures, L blocked.**\n\n## Version File Reference\n\nThe version is defined in `eng/Versions.props`:\n\n```xml\n<PropertyGroup>\n  <VersionPrefix>18.3.3</VersionPrefix>\n</PropertyGroup>\n```\n\n| Branch Pattern | VersionPrefix Format | Bump Needed? | Pre-release Label |\n|---------------|---------------------|-------------|------------------|\n| `main`        | `Major.Minor.0`     | No          | `preview`        |\n| `vs*`         | `Major.Minor.Patch` | Yes (+1)    | `servicing` or `preview` |\n\n## Troubleshooting\n\n| Problem | Solution |\n|---------|----------|\n| `updatePullRequestBranch` fails | PR may have conflicts GitHub can't auto-resolve — fall back to local `git merge` |\n| Push rejected after version bump | Branch was updated by maestro; re-fetch and retry |\n| VersionPrefix has no patch component (e.g. `18.6.0` on main) | This is main — skip the bump |\n| OneLoc PR flagged as \"reverting translations\" | Do NOT merge — wait for updated translations |\n",".github/skills/optimizing-msbuild-performance/SKILL.md":"---\nname: optimizing-msbuild-performance\ndescription: 'Guides performance optimization for MSBuild engine code. Consult when working on hot paths in evaluation or execution, reducing allocations, choosing collection types, handling strings efficiently, modifying Expander.cs or Evaluator.cs, using Span-based APIs and stackalloc, caching values, or profiling build performance. Also applies when reviewing PRs for performance regression.'\nargument-hint: 'Describe the performance-sensitive code area or optimization goal.'\n---\n\n# MSBuild Performance Guidelines\n\nMSBuild evaluates and builds thousands of projects in enterprise solutions. Performance is an architectural concern, not an afterthought.\n\n## Guiding Principle\n\n**Profile before optimizing; measure, do not guess.** Use BenchmarkDotNet, the evaluation profiler (`/profileevaluation`), or ETW traces to identify actual bottlenecks before optimizing.\n\nSee [evaluation-profiling.md](../../../documentation/evaluation-profiling.md) and [General_perf_onepager.md](../../../documentation/specs/proposed/General_perf_onepager.md) for profiling tools.\n\n## Allocation Awareness on Hot Paths\n\nThe evaluation and execution engines process millions of operations per build. Unnecessary allocations cause GC pressure that compounds across large solutions.\n\n### Rules\n\n1. **Avoid LINQ in hot paths.** `Where`, `Select`, `Any`, `First` all allocate enumerator objects and delegate closures. Use `foreach` loops instead.\n\n   ```csharp\n   // BAD — allocates iterator + delegate\n   var match = items.FirstOrDefault(i => i.Name == name);\n\n   // GOOD — zero allocations\n   foreach (var item in items)\n   {\n       if (item.Name == name) { /* found */ break; }\n   }\n   ```\n\n2. **Avoid string allocations in formatting.** Use `string.Concat`, interpolation with `Span<T>`, or `StringBuilder` reuse — not `string.Format` on hot paths.\n\n3. **Prefer `Span<T>` and `stackalloc` for short-lived buffers** when parsing or slicing strings. Avoid `Substring` when you only need to compare or inspect a portion.\n\n4. **Cache computed values.** If a value is computed in a loop but doesn't change, hoist it out. If it's expensive and reused across calls, use a field or `Lazy<T>`.\n\n## String Comparison Rules\n\nMSBuild property, item, and target names are **case-insensitive**. Getting this wrong causes subtle bugs and perf issues.\n\n| Scenario | Use |\n|----------|-----|\n| Property/item/target names | `MSBuildNameIgnoreCaseComparer` |\n| General MSBuild identifiers | `StringComparison.OrdinalIgnoreCase` |\n| Dictionary keys for MSBuild names | `MSBuildNameIgnoreCaseComparer` as comparer |\n| File paths on Windows | `StringComparison.OrdinalIgnoreCase` |\n| File paths on Linux | `StringComparison.Ordinal` |\n\n**Never** use `ToLower()`/`ToUpper()` for comparisons — they allocate a new string every time.\n**Never** use `CurrentCulture` for MSBuild identifiers — build behavior must not vary by locale.\n\n## Collection Type Selection\n\n| Access Pattern | Recommended Type |\n|---------------|-----------------|\n| Small fixed set (< ~8 items) | Array or `ReadOnlySpan<T>` |\n| Build-once, read-many | `ImmutableArray<T>` (not `ImmutableList<T>`) |\n| Keyed lookup, many items | `Dictionary<TKey, TValue>` with appropriate comparer |\n| Keyed lookup, few items (< 5) | Linear scan over array (cache-friendly, avoids dict overhead) |\n| Concurrent reads, rare writes | `ImmutableDictionary` or snapshot pattern |\n| Ordered iteration needed | `List<T>` or array; avoid `HashSet<T>` if order matters |\n\n**`ImmutableList<T>` is almost never the right choice** — it has O(log n) access vs O(1) for `ImmutableArray<T>`.\n\n## Hot Path Identification\n\nThese areas are performance-critical and require extra scrutiny:\n\n- **`Expander.cs`** — property/item/metadata expansion during evaluation\n- **`Evaluator.cs`** — project evaluation orchestration\n- **`ItemSpec.cs` / `LazyItemEvaluator.cs`** — item evaluation and globbing\n- **`TaskExecutionHost.cs`** — task parameter marshaling\n- **File I/O paths** — `FileMatcher.cs`, glob operations, project loading\n\n### Inlining Considerations\n\nFor extremely hot methods, consider:\n- `[MethodImpl(MethodImplOptions.AggressiveInlining)]` for small methods called millions of times\n- Avoiding virtual dispatch in inner loops\n- Using `struct` enumerators to avoid heap allocation\n\n## Evaluation Performance Specifics\n\n- **Import chain depth** affects evaluation time linearly. Minimize unnecessary imports.\n- **Glob patterns** are evaluated per-project. Overly broad globs (`**/*`) are expensive.\n- **Condition evaluation** should use short-circuit logic — put cheap checks first.\n- **Property functions** (`$([System.IO.Path]::...)`) are interpreted and slower than built-in operations.\n\n## Anti-Patterns\n\n| Anti-Pattern | Why It's Bad | Fix |\n|-------------|-------------|-----|\n| `items.Count() > 0` | Enumerates entire collection | `items.Any()` or check `.Count` property |\n| `string.Format` in log messages at Low importance | Allocates even if message is filtered | Use structured logging or guard with verbosity check |\n| `new List<T>(enumerable).ToArray()` | Double allocation | `enumerable.ToArray()` directly |\n| `dict.ContainsKey(k) then dict[k]` | Double lookup | `dict.TryGetValue(k, out var v)` |\n| Regex in a loop without `RegexOptions.Compiled` | Reinterprets pattern each time | Compile or use static `Regex` field |\n",".github/skills/pipelines-health-check/SKILL.md":"---\nname: pipelines-health-check\ndescription: Check health of MSBuild CI pipelines, VS repo PR insertion statuses, and VMR codeflow PRs. Use when asked about pipeline health, build failures, infrastructure issues, CI status, insertion PR status, VMR codeflow status, or for periodic health monitoring.\n---\n\n# Pipelines & PR Health Check\n\nThis skill checks the health of MSBuild's CI pipelines, the status of insertion PRs in the VS repository, and the health of VMR codeflow PRs that flow MSBuild source into the dotnet/dotnet unified repository.\n\n## When to Use\n\n- User asks about MSBuild pipeline health, CI status, or build failures\n- User asks about VS insertion PR status or whether insertions are going through\n- User asks about VMR codeflow PRs, dotnet/dotnet PR status, or dotnet-unified-build pipeline\n- User asks to check if there are failing checks on PRs\n- User asks for a health check or status overview\n- Periodic monitoring requests\n\n## Prerequisites\n\n- `az` CLI must be installed and authenticated (`az login` with access to the DevDiv organization)\n- Azure DevOps extension for `az` must be installed: `az extension add --name azure-devops`\n- PowerShell 5.1+ or PowerShell Core\n\n### Optional: WorkIQ (for infrastructure issue investigation)\n\n[WorkIQ](https://www.npmjs.com/package/@microsoft/workiq) is an MCP server / CLI that can query Microsoft 365 data (people, emails, Teams, documents) to find **service ownership, contacts, and incident context** when pipeline failures are caused by infrastructure issues outside MSBuild's control.\n\n**Check availability:**\n```powershell\nworkiq version\n# Expected: 0.2.x or later\n```\n\n**If not installed**, set it up:\n```powershell\n# Install globally (use --registry if your .npmrc redirects @microsoft scope to GitHub Packages)\nnpm install -g @microsoft/workiq --registry https://registry.npmjs.org\n\n# Accept the EULA (required once)\nworkiq accept-eula\n```\n\nWorkIQ is not required for the core health check. If unavailable, the skill will still work — it will simply skip the ownership lookup and suggest manual investigation or offer to help install WorkIQ.\n\n## Reference Information\n\n### Pipelines\n\n| Pipeline | Org/Project | ID | Purpose |\n|----------|-------------|----|---------|\n| MSBuild | devdiv/DevDiv | 9434 | Main CI pipeline — builds and tests on every commit to main |\n| MSBuild-OptProf | devdiv/DevDiv | 17389 | Optimization/profiling pipeline — runs on schedule |\n| dotnet-unified-build | dnceng-public/public | — | VMR build pipeline — runs on codeflow PRs in dotnet/dotnet |\n\n### Key URLs\n\n- MSBuild pipeline: `https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434`\n- OptProf pipeline: `https://devdiv.visualstudio.com/DevDiv/_build?definitionId=17389`\n- VS PRs assigned to MSBuild: `https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequests?_a=active&assignedTo=66cc9d27-aef7-4399-ba2c-3dccb4489098`\n- VMR codeflow PRs: `https://github.com/dotnet/dotnet/pulls?q=is:pr+is:open+\"Source+code+updates+from+dotnet/msbuild\"`\n- dotnet-unified-build (public): `https://dev.azure.com/dnceng-public/public/_build`\n\n## Phase 1: Collect Data & Present Overview Table\n\n### Step 1: Run all data collection scripts\n\nRun these three scripts from the repository root **in parallel**. They output JSON to stdout. Each script may take 1–3 minutes depending on the number of PRs and pipeline runs to fetch, so use an `initial_wait` of **120 seconds** or higher.\n\n```powershell\n# Pipeline health (checks both MSBuild and MSBuild-OptProf)\n$pipelineJson = & .\\.github\\skills\\pipelines-health-check\\check-pipeline-health.ps1\n\n# VS PR status (checks active non-Experimental PRs and last merged PR)\n$prJson = & .\\.github\\skills\\pipelines-health-check\\check-vs-pr-status.ps1\n\n# VMR codeflow status (checks open codeflow PRs from msbuild and their dotnet-unified-build runs)\n$vmrJson = & .\\.github\\skills\\pipelines-health-check\\check-vmr-codeflow.ps1\n```\n\nAll scripts write progress messages to stderr (`Write-Host`) and the JSON result to stdout.\n\nThe scripts sanitize error messages (stripping control characters, truncating to 500 chars) so the JSON output can be parsed directly with `ConvertFrom-Json` without additional cleanup.\n\n### Step 2: Present the overview table IMMEDIATELY\n\nParse the JSON outputs and render status overview tables to the user **before** doing any deeper investigation. This gives the user instant visibility.\nPresent ALL tables - for both pipelines and for the VS insertion PRs. Do not omit any of those unless explicitly asked by user just for some specific overview.\n\n#### Pipeline Health Table\n\nFor each pipeline in the JSON output, render one row:\n\n| Pipeline | Last Success | Age | Recent Runs | Status |\n|----------|-------------|-----|-------------|--------|\n| {pipelineName} ({pipelineId}) | {lastSuccessfulRun.finishTime} | {lastSuccessfulRun.ageHours}h | emoji sequence | status emoji + label |\n\n**Recent Runs column:** Show an emoji for each run in `recentRuns` array (newest first):\n- `✅` for `succeeded`\n- `❌` for `failed`\n- `⏳` for `inProgress`\n- `⚪` for `canceled` or other\n\n**Status column** — derive from `healthSummary` and `lastSuccessfulRun.ageHours`:\n- `✅ HEALTHY` — healthSummary starts with \"HEALTHY\"\n- `⚠️ FLAKY` — healthSummary starts with \"FLAKY\"\n- `🔴 UNHEALTHY` — healthSummary starts with \"UNHEALTHY\"\n- Add `⚠️` if ageHours > 24, `🔴` if ageHours > 48 (even if some runs succeed, stale success is a concern)\n\n#### VS Insertion PRs Table (non-Experimental)\n\nFor each PR in the `prs` array:\n\n| PR | Title | Checks ✅ | Checks ⏳ | Checks ❌ | Status |\n|----|-------|-----------|-----------|-----------|--------|\n| [{id}](url) | {title} (truncated) | {checks.succeeded} | {checks.pending} | {checks.failed} | status |\n\n**Status column:**\n- `🔴 Failing` — if `actionNeeded` is true (has failed required checks)\n- `⏳ Running` — if `checks.pending > 0` and no failures\n- `✅ Green` — if all checks succeeded or notApplicable\n\n#### Last Merged Insertion Row\n\n| Last Merged PR | Date | Age | Status |\n|---------------|------|-----|--------|\n| [{lastMergedPr.id}](lastMergedPr.url) | {lastMergedPr.closedDate} | {ageDays} days | status |\n\n**Status:**\n- `✅ Recent` — ageHours ≤ 48 (≤ 2 business days)\n- `⚠️ Getting stale` — ageHours > 48 and ≤ 96\n- `🔴 Stale insertion` — ageHours > 96 (> 4 business days)\n\n**Note on weekends:** When computing business-day age, be aware that weekends inflate the hour count. If today is Monday and the last merge was Friday, that's ~72h but only 1 business day. Mention this nuance to the user if the age seems borderline.\n\n#### VMR Codeflow PRs Table\n\nFor each codeflow PR in the `codeflowPRs` array from `$vmrJson`:\n\n| Codeflow PR | Age | Pipeline Runs | Upstream PRs | Status |\n|-------------|-----|---------------|--------------|--------|\n| [#{prNumber}](prUrl) | {prAge}h | emoji sequence from pipelineRuns | count | status emoji + label |\n\n**Pipeline Runs column:** Show an emoji for each run in `pipelineRuns` (newest first):\n- `✅` for `result == \"succeeded\"`\n- `❌` for `result == \"failed\"`\n- `⏳` for `status == \"inProgress\"`\n- `⚪` for other/no runs\n\n**Status column** — derive from `healthSummary`:\n- `✅ HEALTHY` — healthSummary starts with \"HEALTHY\"\n- `🔄 IN PROGRESS` — healthSummary starts with \"IN_PROGRESS\" or \"RETRYING\"\n- `🔴 FAILING` — healthSummary starts with \"UNHEALTHY\"\n- `⚠️ MIXED` — healthSummary starts with \"MIXED\"\n- `❓ UNKNOWN` — no pipeline runs found\n\n**If a PR has failures**, also render a failure details sub-table:\n\n| Failed Job | Failed Task | Error Category | Related Upstream PRs |\n|------------|------------|----------------|---------------------|\n| {job.name} | {task.name} | {category} | PR links from failureCorrelation |\n\nThe `failureCorrelation` array maps each failed build to error categories and potentially related upstream PRs (matched by title keywords). This helps quickly identify which msbuild change likely caused a VMR build failure.\n\n**Upstream PRs list:** For each codeflow PR, list the included upstream msbuild PRs:\n\n> **Included changes:** [#13175](url) Add App Host Support, [#13306](url) IBuildEngine callbacks, ...\n\n### Step 3: Identify problems\n\nAfter rendering the table, build a list of distinct problems. A \"problem\" is any of:\n\n1. **Pipeline failure** — A pipeline whose latest run on main failed, especially if `lastSuccessfulRun.ageHours > 24`\n2. **PR check failure** — An active non-Experimental PR that has `actionNeeded: true` (failed required checks)\n3. **Stale insertion** — `lastMergedPr.ageHours > 48` (no successful insertion in >2 business days)\n4. **All checks pending** — A PR where all checks are still pending/queued (may indicate a stuck pipeline or queue issue)\n5. **VMR codeflow failure** — A codeflow PR whose `healthSummary` starts with \"UNHEALTHY\" (dotnet-unified-build failing)\n6. **VMR codeflow stale** — A codeflow PR older than 48 hours with no successful pipeline run\n\nIf there are **no problems**, report `✅ ALL CLEAR — pipelines healthy, PRs on track, insertions flowing, VMR codeflow green` and stop. Do not proceed to Phase 2.\n\n## Phase 2: Investigate Problems via Subagents\n\nFor **each distinct problem** identified in Step 3, launch a **separate subagent** to perform DEEP, DETAILED investigation (use `#tool:agent/runSubagent` to run the investigation tasks). Fire them in parallel when possible. Use the <agent> template below to seed them. \n\n<subagent>\n\n### Subagent prompt templates\n\n#### For pipeline failures\n\n```\nInvestigate why the Azure DevOps pipeline \"{pipelineName}\" (ID: {pipelineId}) is failing.\n\nRecent failed runs on branch {branch}:\n{for each failed run, list: Run ID, start time, URL, and the failedTasks with their error messages}\n\nLast successful run: {lastSuccessfulRun.finishTime} ({ageHours} hours ago)\nURL: {lastSuccessfulRun.url}\n\nTasks:\n1. Categorize each failure as one of:\n   - BUILD ERROR: compilation failures, test failures, task execution errors in MSBuild code\n   - CONFIG/PERMISSION: signing errors, NuGet authentication, certificate issues, feed access\n   - INFRA/TRANSIENT: errors indicating unavailability or outage of services or resources\n2. Check if all recent failures share the same root cause or if there are different issues\n3. If infra/transient: suggest retrying the pipeline (provide the pipeline URL)\n4. If build error: \n  - Check the `For build errors` section below on how to investigate build errors with binlogs\n  - identify which component/task is failing and check recent commits to main to try to identify offending one.\n5. If infrastructure issues:\n  - Try to distill the exact reason for the issue, check if there are other failing pipelines with the same issue or any open bugs for the issue.\n  - **Use WorkIQ** to find the owning team and contacts. Check if `workiq` CLI is available (`workiq version`).\n    - If available, run: `workiq ask -q \"Who owns the {failing service/task name} service in Microsoft DevDiv? Who should be contacted about {brief error description}?\"`\n    - Include the WorkIQ response in your findings — it typically returns team names, distribution lists, contact people, and escalation paths.\n    - You can also ask WorkIQ about known outages: `workiq ask -q \"Are there any known outages or incidents for {service name} in Azure DevOps?\"`\n    - If WorkIQ is NOT available, note this in your report and suggest the user install it:\n      ```\n      npm install -g @microsoft/workiq --registry https://registry.npmjs.org\n      workiq accept-eula\n      ```\n  - Put together a concise overview of the issue, along with links to the failure messages, the owning team/contacts from WorkIQ, and suggested next steps.\n\nReturn: A comprehensive root cause analysis with category, explanation, links to failure messages, ownership info (from WorkIQ if available), and recommended action.\n```\n\n#### For PR check failures\n\n```\nInvestigate failing checks on VS insertion PR #{prId}: \"{prTitle}\"\nPR URL: {prUrl}\n\nFailed checks:\n{for each item in checks.failedChecks: genre, name, description, isRequired}\n\nPending checks (still running):\n{for each item in checks.pendingChecks: genre, name, description, isRequired}\n\nPipeline health context:\n{brief summary of pipeline health from Phase 1 — are pipelines also failing?}\n\nTasks:\n1. Identify which failed checks are required vs optional\n2. If required checks are failing, determine if this could be related to pipeline failures (same root cause)\n3. If checks are just pending/queued, note that they may still be running and suggest waiting\n4. Recommend specific actions: retry checks, investigate pipeline, or wait\n5. If check is failing - try to traverse the chain of called pipelines to the actual error, then: \n  - Check the `For build errors` section below on how to investigate build errors with binlogs\n  - identify which component/task is failing and check recent commits to msbuild main to try to identify offending one.\n\nReturn: Which checks need attention, likely cause, and recommended action.\n```\n\n#### For stale insertion\n\n```\nInvestigate why MSBuild insertions into VS appear stale.\n\nLast successfully merged non-Experimental PR: #{lastMergedPr.id} \"{lastMergedPr.title}\"\n  Merged: {lastMergedPr.closedDate} ({ageDays} days ago)\n  URL: {lastMergedPr.url}\n\nCurrently active non-Experimental PRs:\n{for each PR: id, title, url, checks summary, actionNeeded}\n\nPipeline health:\n{brief pipeline health summary}\n\nTasks:\n1. Check if there are active non-Experimental PRs waiting — if none, the issue may be that no insertion was triggered\n2. If there are active PRs with failing checks, identify if those failures are blocking the insertion\n3. If there are active PRs with all checks pending, they may just need time\n4. Correlate with pipeline health — if the CI pipeline is broken, insertions can't succeed\n5. Recommend specific actions to unblock\n\nReturn: Explanation of why insertion appears stuck and what to do about it.\n```\n\n#### For VMR codeflow failures\n\n```\nInvestigate failing dotnet-unified-build pipeline for VMR codeflow PR #{prNumber}: \"{prTitle}\"\nPR URL: {prUrl}\nPR Branch: {prBranch}\n\nThis is a codeflow PR that brings MSBuild source changes into the dotnet/dotnet VMR (Virtual Monolithic Repository).\nThe dotnet-unified-build pipeline runs in the dnceng-public/public Azure DevOps org.\n\nFailed pipeline runs:\n{for each failed run in pipelineRuns: buildId, buildNumber, URL, stages, failedJobs with their failedTasks and errors}\n\nFailure categories detected: {failureCorrelation[].categories}\n\nUpstream MSBuild PRs included in this codeflow:\n{for each PR in upstreamPRs: number, title, url, merged status}\n\nFailure-to-change correlation (from script):\n{for each item in failureCorrelation: buildId, categories, relatedUpstreamPRs}\n\nTasks:\n1. For each failed job, examine the error messages and categorize:\n   - TASK_HOST: MSB4216 errors about MSBuild not finding the task host executable — typically caused by changes to MSBuild's app host, node launching, or SDK layout\n   - SOURCE_BUILD_TASK_HOST: Same as TASK_HOST but in the source-only build (CentOS offline) — the previously-source-built SDK doesn't have the expected MSBuild executable\n   - COMPILATION: CS/VB/FS compilation errors — a code change broke the build\n   - BUILD_COMMAND: MSB3073 \"exited with code N\" — a build script or test failed\n   - NUGET_AUTH/SIGNING/TIMEOUT/RESOURCE: Infrastructure issues\n2. Use the failure-to-change correlation to identify which upstream MSBuild PR most likely caused each failure:\n   - Check if the error is in an area touched by one of the upstream PRs\n   - If TASK_HOST errors: look for PRs touching NodeLauncher, NodeProvider, task host, app host, or BuildEnvironmentHelper\n   - If COMPILATION errors: check which files the upstream PRs modified and whether any could cause the compilation break\n   - If test failures: identify which test is failing and which upstream PR likely affects that code path\n3. Check if there is an in-progress retry build that might resolve the issue\n4. If the failure appears to be an infrastructure issue (not caused by MSBuild changes), note that\n5. For build errors, check the `For build errors` section on how to investigate with binlogs\n6. Recommend specific actions:\n   - If an upstream PR is clearly at fault, suggest reverting it or filing a fix\n   - If a VMR-side fix is needed (e.g., SDK layout change), describe what needs to change\n   - If a retry might help (transient infra), suggest re-running the pipeline\n   - If the issue is already being retried (in-progress build), suggest waiting\n\nReturn: Root cause analysis mapping each failure to the likely upstream PR, with explanation, links, and recommended action.\n```\n\n#### For build errors\n\nTasks:\n1. Try to find a .binlog file(s) in the build or step artifacts and fetch it\n2. Ensure to acquire the [binlog-failure-analysis skill](https://github.com/ViktorHofer/dotnet-skills/blob/main/msbuild-skills/skills/binlog-failure-analysis/SKILL.md) together with the binlog-mcp (spawn via `dnx -y baronfel.binlog.mcp@0.0.13`)\n3. Use the binlog analysis skill and mcp to analyse the binlog(s) you found and analyse problems from those\n\n</subagent>\n\n## Phase 3: Final Report\n\nAfter all subagent results return, present the findings below the overview table under a **\"🔍 Problems & Recommendations\"** heading. For each problem:\n\n```markdown\n### Problem: {brief title}\n**Category:** {INFRA | BUILD | CONFIG | PR_CHECKS | STALE_INSERTION | VMR_CODEFLOW}\n**Details:** {subagent's explanation}\n**Ownership:** {owning team, contacts, DL from WorkIQ — include only for INFRA/CONFIG issues}\n**Recommended Action:** {subagent's recommendation}\n```\n\nIf all problems are infra/transient, add a note: *\"All current issues appear to be infrastructure-related. Consider retrying the pipelines and checking again in 30 minutes.\"*\n\n## Troubleshooting\n\n### \"az: command not found\" or \"az account get-access-token\" fails\nThe `az` CLI is not installed or not authenticated. Run `az login` first.\n\n### Scripts return empty arrays\n- Check that you have access to the DevDiv organization\n- The branch filter defaults to `main` — if checking a different branch, pass `-Branch <name>` to the pipeline script\n\n### PR statuses all show as \"pending\"\nThis is normal for newly created PRs. The checks take time to queue and run. If checks are pending for more than a few hours, this may indicate a stuck pipeline or queue issue.\n\n### Timeout or rate limiting\nIf the scripts take a long time or fail with 429 errors, Azure DevOps may be rate-limiting. Wait a minute and retry.\n\n### WorkIQ not found or EULA not accepted\nIf `workiq version` fails, install it:\n```powershell\nnpm install -g @microsoft/workiq --registry https://registry.npmjs.org\nworkiq accept-eula\n```\nNote: If your `.npmrc` redirects the `@microsoft` scope to GitHub Packages, use `--registry https://registry.npmjs.org` to override, or pass `--userconfig` pointing to a clean `.npmrc`.\n\n### WorkIQ returns empty or unhelpful results\nWorkIQ queries Microsoft 365 data (Outlook, Teams, SharePoint). Results depend on your account's access and the data available in your tenant. Try rephrasing the question or being more specific about the service name. Example queries that work well:\n- `workiq ask -q \"Who owns the MicroBuild service in Microsoft?\"`\n- `workiq ask -q \"Who owns the CloudBuild signing service in DevDiv?\"`\n- `workiq ask -q \"Who should I contact about NuGet feed authentication failures in Azure DevOps?\"`\n\n### VMR codeflow script returns no PRs\n- Verify there are actually open codeflow PRs: check `https://github.com/dotnet/dotnet/pulls?q=is:pr+is:open+\"Source+code+updates+from+dotnet/msbuild\"`\n- Verify `gh` CLI is authenticated: run `gh auth status`\n\n### VMR pipeline runs not found\n- The dotnet-unified-build pipeline runs in `dnceng-public/public`, which is publicly accessible\n- If no runs appear, the pipeline may not have been triggered yet for that PR\n- Codeflow PRs may take a few minutes after creation before CI triggers\n\n### JSON output too large or contains unexpected characters\n- Error messages from Azure DevOps timelines can contain Windows paths, control characters, and multi-KB stack traces\n- The scripts sanitize and truncate these to 500 characters — if you still see issues, check that you're running the latest version of the scripts\n- Use `ConvertFrom-Json` in PowerShell or `json.loads()` in Python to parse the output; avoid manual string manipulation\n",".github/skills/project-management/SKILL.md":"---\nname: project-management\ndescription: 'GitHub issue and project-board management for the dotnet/msbuild repo. Use when asked to file/triage/update issues, post comments, amend issue bodies, move sprints, bulk-update project board fields, or audit items by sprint/status/assignee.'\nargument-hint: 'Manage issues and project board entries for dotnet/msbuild.'\n---\n\n# Project Management for dotnet/msbuild\n\nThis skill covers everything you need to manage issues and the project board for the MSBuild repository: querying, commenting, editing, creating issues, and bulk operations on the project board (sprints, status, assignees, etc.).\n\n## Where things live\n\n- **Repository:** `dotnet/msbuild` — all issues, PRs, and discussions.\n- **Project board:** [`dotnet/projects/117`](https://github.com/orgs/dotnet/projects/117) — title \"MSBuild\". This is the canonical project board for the team. Most issues created in `dotnet/msbuild` are auto-added or manually added here for sprint planning.\n- **Sprints** are tracked via the **Sprint** iteration field on project 117. Iteration titles use the format `2026 May I`, `2026 May II`, `2026 June I`, etc. (roughly two-week iterations).\n\n> Note: there are/were other auxiliary project boards for specific initiatives. Use project 117 unless the user explicitly points at another one.\n\n### Stable identifiers for project 117\n\n| Identifier | Value |\n|---|---|\n| Project node ID | `PVT_kwDOAIt-yc4ABM5F` |\n| Sprint field ID (iteration) | `PVTIF_lADOAIt-yc4ABM5FzgAtMGI` |\n\nThese IDs are stable but can be re-derived if the project is ever recreated or migrated:\n```bash\n# Project node ID\ngh api graphql -f query='{ organization(login:\"dotnet\") { projectV2(number:117) { id } } }'\n\n# All field IDs (Sprint, Status, Area, Priority, …)\ngh project field-list 117 --owner dotnet --format json | jq '.fields[] | {id,name,type}'\n```\n\n## Tooling preference\n\nFor GitHub work, prefer `gh` CLI over MCP tools (per repo convention).\n\n- Use `gh issue ...` for issue-level operations (create / view / comment / edit / close).\n- Use `gh project ...` for project-board CRUD.\n- Use `gh api graphql` for anything `gh` doesn't expose directly (sprint values per item, project-level field values, bulk reads).\n\nThe default `gh` token may not include the `project` scope. If a `gh project ...` write call fails with a scope error, ask the user to run:\n```bash\ngh auth refresh -s project\n```\n\n## Common single-issue actions\n\n```bash\n# View\ngh issue view 13315 --repo dotnet/msbuild --json title,body,state,assignees,labels\n\n# Comment (use --body-file to avoid shell-quoting traps with multi-line markdown)\ngh issue comment 13315 --repo dotnet/msbuild --body-file ./comment.md\n\n# Amend body (overwrite — there is no append; use view → edit → write)\ngh issue edit 13597 --repo dotnet/msbuild --body-file ./new-body.md\n\n# Create\ngh issue create --repo dotnet/msbuild --title \"Title\" --body-file ./body.md\n\n# Add labels / assignees\ngh issue edit 13315 --repo dotnet/msbuild --add-label \"Area: Server\" --add-assignee someuser\n```\n\nWhen drafting comments / bodies that summarize internal discussions:\n- Use neutral, team-voice language (\"the team agreed\", \"open question\").\n- Scrub names, \"X said Y\", room/agent identifiers, and anything resembling internal politics.\n- Always write to a file and use `--body-file`. Don't try to inline multi-line markdown.\n\n## Querying issue → project field values (sprint, status, project-level assignees)\n\n`gh issue view` does **not** return project-board field values. Use GraphQL via the `repository → issue → projectItems` path:\n\n```bash\ngh api graphql -f query='\nquery {\n  r: repository(owner:\"dotnet\", name:\"msbuild\") {\n    issue(number: 13315) {\n      id\n      projectItems(first: 5) {\n        nodes {\n          id\n          project { number title }\n          sprint:  fieldValueByName(name: \"Sprint\")  { ... on ProjectV2ItemFieldIterationValue   { title iterationId } }\n          status:  fieldValueByName(name: \"Status\")  { ... on ProjectV2ItemFieldSingleSelectValue { name optionId } }\n        }\n      }\n    }\n  }\n}'\n```\n\n> `fieldValueByName` returns `null` when the field has no value set on that item — handle accordingly.\n\n## Bulk / board-wide queries\n\nThe `gh project item-list` CLI command has internal limits and is unreliable for large boards (project 117 is in the thousands of items). **Use GraphQL with pagination.**\n\n```python\nimport subprocess, json\n\nPROJECT_NUMBER = 117\nORG = \"dotnet\"\n\nresults = []\ncursor = None\nwhile True:\n    after = f', after: \"{cursor}\"' if cursor else ''\n    query = f'''query {{\n      organization(login: \"{ORG}\") {{\n        projectV2(number: {PROJECT_NUMBER}) {{\n          items(first: 100{after}) {{\n            pageInfo {{ hasNextPage endCursor }}\n            nodes {{\n              id\n              fieldValues(first: 20) {{\n                nodes {{\n                  ... on ProjectV2ItemFieldIterationValue {{\n                    field {{ ... on ProjectV2IterationField  {{ name }} }}\n                    title\n                  }}\n                  ... on ProjectV2ItemFieldUserValue {{\n                    field {{ ... on ProjectV2Field           {{ name }} }}\n                    users(first: 10) {{ nodes {{ login }} }}\n                  }}\n                  ... on ProjectV2ItemFieldSingleSelectValue {{\n                    field {{ ... on ProjectV2SingleSelectField {{ name }} }}\n                    name\n                  }}\n                }}\n              }}\n              content {{\n                ... on Issue        {{ number title }}\n                ... on PullRequest  {{ number title }}\n                ... on DraftIssue   {{ title }}\n              }}\n            }}\n          }}\n        }}\n      }}\n    }}'''\n    try:\n        r = subprocess.run(['gh','api','graphql','-f',f'query={query}'],\n                           capture_output=True, text=True, check=True)\n    except subprocess.CalledProcessError as e:\n        raise RuntimeError(f\"GraphQL query failed: {e.stderr}\") from e\n    page = json.loads(r.stdout)['data']['organization']['projectV2']['items']\n    for node in page['nodes']:\n        sprint = status = None\n        assignees = []\n        for fv in node.get('fieldValues', {}).get('nodes', []):\n            if not fv:                              # null entries for unset fields\n                continue\n            fname = (fv.get('field') or {}).get('name', '')\n            if fname == 'Sprint':\n                sprint = fv.get('title')\n            elif fname == 'Status':\n                status = fv.get('name')\n            elif fname == 'Assignees':\n                assignees = [u['login'] for u in (fv.get('users') or {}).get('nodes', [])]\n        results.append({'id': node['id'], 'sprint': sprint, 'status': status,\n                        'assignees': assignees, 'content': node.get('content')})\n    if not page['pageInfo']['hasNextPage']:\n        break\n    cursor = page['pageInfo']['endCursor']\n```\n\n## Updating project-board field values\n\n### Set a sprint (iteration) — first look up iteration IDs\n\nIteration IDs are short hex strings (e.g., `303c2425`). They are **not** the same across projects; always look them up from project 117.\n\n```bash\ngh api graphql -f query='\nquery {\n  node(id: \"PVTIF_lADOAIt-yc4ABM5FzgAtMGI\") {\n    ... on ProjectV2IterationField {\n      configuration {\n        iterations           { id title startDate }\n        completedIterations  { id title startDate }\n      }\n    }\n  }\n}' | jq '.data.node.configuration | (.iterations + .completedIterations)[] | {id,title}'\n```\n\n### Single-item field update — `gh project item-edit`\n\n```bash\n# Iteration / sprint\ngh project item-edit \\\n  --project-id PVT_kwDOAIt-yc4ABM5F \\\n  --id <PROJECT_ITEM_ID> \\\n  --field-id PVTIF_lADOAIt-yc4ABM5FzgAtMGI \\\n  --iteration-id <ITERATION_ID>\n\n# Single-select (Status, Area, Priority): use --single-select-option-id\n# Text: --text  | Number: --number  | Date: --date YYYY-MM-DD\n```\n\n### Single-item field update — GraphQL mutation (works the same)\n\n```bash\ngh api graphql -f query='\nmutation {\n  updateProjectV2ItemFieldValue(input: {\n    projectId: \"PVT_kwDOAIt-yc4ABM5F\",\n    itemId:    \"<PROJECT_ITEM_ID>\",\n    fieldId:   \"PVTIF_lADOAIt-yc4ABM5FzgAtMGI\",\n    value:     { iterationId: \"<ITERATION_ID>\" }\n  }) { projectV2Item { id } }\n}'\n```\n\n### Adding a freshly created issue to project 117\n\nA newly-created issue is not always auto-added. To add and set its sprint:\n\n```bash\nISSUE_NODE_ID=$(gh api graphql -f query='\n  query { repository(owner:\"dotnet\",name:\"msbuild\") { issue(number:13707) { id } } }' \\\n  | jq -r '.data.repository.issue.id')\n\nITEM_ID=$(gh api graphql -f query='\n  mutation { addProjectV2ItemById(input:{\n      projectId:\"PVT_kwDOAIt-yc4ABM5F\",\n      contentId:\"'\"$ISSUE_NODE_ID\"'\"\n  }) { item { id } } }' | jq -r '.data.addProjectV2ItemById.item.id')\n\ngh api graphql -f query='\n  mutation { updateProjectV2ItemFieldValue(input:{\n      projectId:\"PVT_kwDOAIt-yc4ABM5F\",\n      itemId:\"'\"$ITEM_ID\"'\",\n      fieldId:\"PVTIF_lADOAIt-yc4ABM5FzgAtMGI\",\n      value:{iterationId:\"<ITERATION_ID>\"}\n  }) { projectV2Item { id } } }'\n```\n\n## Common errors and gotchas\n\n### 1. `Field 'fieldValueByName' has an argument conflict`\n\n`gh api graphql --paginate` (and several jq-style aliases) re-uses field names. If you call `fieldValueByName(name:\"Sprint\")` and `fieldValueByName(name:\"Status\")` in the same selection set without aliases, you'll get:\n\n```\nField 'fieldValueByName' has an argument conflict: {name:\"Sprint\"} or {name:\"Status\"}?\n```\n\nFix: alias each call.\n\n```graphql\nsprint: fieldValueByName(name: \"Sprint\")  { ... }\nstatus: fieldValueByName(name: \"Status\")  { ... }\n```\n\n### 2. `gh api graphql --paginate` produces concatenated JSON, not one document\n\nEach page is emitted as a separate JSON object on stdout; piping straight into `jq` fails with \"Invalid numeric literal\". Either:\n- paginate manually in a loop (preferred for large reads), or\n- use `jq --slurp` to read the stream as an array.\n\n### 3. \"Selections can't be made directly on unions\"\n\n`field` on a project field value is a union; you can't select `name` directly. Use a typed inline fragment per concrete field type (`ProjectV2Field`, `ProjectV2IterationField`, `ProjectV2SingleSelectField`, …).\n\n### 4. Issue-level assignees ≠ project-board \"Assignees\" column\n\nThe board's \"No Assignees\" slice is driven by the **project-level Assignees field**, not by `Issue.assignees`. They can diverge. When filtering by board state, always read `ProjectV2ItemFieldUserValue` from `fieldValues`.\n\n### 5. `null` entries in `fieldValues.nodes`\n\nGraphQL returns `null` for items where a particular field is unset. Always guard `if not fv: continue` before dereferencing.\n\n### 6. DraftIssue items lack `number` / `repository`\n\n`content` may be a `DraftIssue` that only exposes `title`, `body`, `assignees`. Code that joins to issue numbers must handle `None`.\n\n### 7. `gh project item-list` is unreliable for large boards\n\nIt silently truncates. Always use the GraphQL pagination loop above instead.\n\n### 8. Missing `project` scope on `gh` token\n\nReads work; writes (`gh project item-edit`, the `addProjectV2ItemById` / `updateProjectV2ItemFieldValue` mutations) fail. Have the user run `gh auth refresh -s project`.\n\n### 9. `gh issue edit --body` overwrites — there is no append\n\nTo \"append\" to a body, fetch with `gh issue view --json body`, append, write back with `--body-file`. For status updates, prefer a comment.\n\n## Sanitization checklist for comments / body edits\n\nWhen summarizing meetings or chat threads into GitHub:\n\n- ✅ Use team voice (\"the team agreed\", \"open question\", \"follow-up needed\").\n- ✅ Keep objective, decision-relevant content (problem statement, scope, exit criteria, repro steps, numbers).\n- ❌ No personal names or \"X said / Y replied\".\n- ❌ No full email addresses of external contributors (use GitHub handles instead).\n- ❌ No internal-only identifiers (room names, recording IDs, agent fleet identifiers, partner-team contractor names).\n- ❌ No speculation about other teams' motives or anything resembling internal politics.\n- ❌ No confidential roadmap dates or unreleased product details unless already public.\n- Always show the user the proposed comment / body before posting (unless they've explicitly preapproved).\n\n## Common workflows\n\n| Goal | Sketch |\n|---|---|\n| Add a status comment to N issues | `for n in 13315 13702 …; do gh issue comment $n --repo dotnet/msbuild --body-file ./c-$n.md; done` |\n| Replace a one-line issue body with a full Context / Exit-criteria / Out-of-scope spec | `gh issue edit <n> --repo dotnet/msbuild --body-file ./body.md` |\n| Move all unassigned items from sprint X to sprint Y | GraphQL pagination loop above + `gh project item-edit --iteration-id` per item |\n| File a new issue and put it in the current sprint | `gh issue create` → `addProjectV2ItemById` → `updateProjectV2ItemFieldValue` |\n| Audit \"what's in the current sprint that has no owner\" | GraphQL pagination loop, filter on `Sprint == \"<current>\"` and empty `Assignees` |\n",".github/skills/release/SKILL.md":"---\nname: release\ndescription: 'Orchestrate an MSBuild release: create the tracking issue, branch, configure DARC channels and subscriptions, bump version in main, final-brand the release branch, insert into VS, and publish post-GA. Covers the full monthly release lifecycle aligned with VS shipping cadence.'\nargument-hint: 'Specify which phase to execute (0-5) and provide required inputs (THIS_RELEASE_VERSION, NEXT_VERSION, etc.)'\n---\n\n# MSBuild Release Orchestration\n\nThis skill guides an agent through the MSBuild release process defined in [`documentation/release-checklist.md`](../../../documentation/release-checklist.md). The checklist is the single source of truth — this skill provides context on how to execute it.\n\n## Overview\n\nMSBuild is a **component** that gets inserted into Visual Studio. VS ships monthly; MSBuild must branch and prepare its bits **before** VS is ready to take them. See the [release process doc](../../../documentation/release.md#how-msbuild-releases-flow-into-vs) for the full timeline diagram.\n\nThe insertion pipeline routes MSBuild branches to VS branches:\n- `main` → VS `main` (daily canary)\n- `vs*` release branch → VS `main` (replaces `main` → `main` after branch snap)\n\nVS handles the progression from `main` → `rel/insiders` → `rel/stable` on its own schedule. MSBuild's responsibility is to have final-branded bits in VS `main` before the insiders snap date.\n\nEach monthly VS release produces:\n- A new `vs*` branch from `main`\n- Final branding on that branch\n- A version bump in `main`\n- DARC channel/subscription updates\n- A VS insertion\n- Post-GA publishing to nuget.org and docs\n\nThe process is organized into **6 timeline-gated phases** (0–5), each with an explicit trigger.\n\n> **Execution model**: This skill is designed for an **interactive Copilot session**. The agent walks through each phase step-by-step, but every command that modifies state (git push, DARC writes, pipeline changes, PR creation) requires **user approval** before execution. Read-only queries (DARC get-*, git log, etc.) can run without approval.\n\n## Required Inputs\n\nBefore starting any phase, ensure you have these values (the user must provide them — version increments are irregular and cannot be computed):\n\n| Input | Example | How to determine |\n|---|---|---|\n| `PREVIOUS_RELEASE_VERSION` | `18.9` | Previous release branch version: the highest `vs*` version lower than `THIS_RELEASE_VERSION` (list candidates with `git branch -r --list 'origin/vs*'`). |\n| `PREVIOUS_RELEASE_EXACT_VERSION` | `18.9.6` | The version the previous release actually shipped as. From the previous release's tracking issue, or `git tag --list 'v18.9.*'`. Used by Phase 5.3a. |\n| `THIS_RELEASE_VERSION` | `18.10` | Current `VersionPrefix` in `eng/Versions.props` (drop `.0`) |\n| `NEXT_VERSION` | `18.11` | User-provided — not computable from current version |\n| `BRANCH_SNAP_DATE` | `YYYY-MM-DD` | From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) — when MSBuild branches `vs*` from main, insertion targets VS `main` |\n| `INSIDERS_SNAP_DATE` | `YYYY-MM-DD` | From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) — when VS snaps `main` → `rel/insiders`; final-branded bits must be in VS `main` before this |\n| `STABLE_SNAP_DATE` | `YYYY-MM-DD` | From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) — when VS promotes `rel/insiders` → `rel/stable` |\n| `VS_SHIP_DATE` | `YYYY-MM-DD` | When VS ships publicly (GA) — triggers post-release tasks |\n| `PACKAGE_VALIDATION_BASELINE_VERSION` | `18.10.0-1.26378.2` | See [How to determine `PACKAGE_VALIDATION_BASELINE_VERSION`](#how-to-determine-package_validation_baseline_version) below — non-trivial: most \"obvious\" picks are wrong. |\n\n> Version examples above track the current cycle (`eng/Versions.props` `VersionPrefix` is `18.10.0`). Dates are intentionally shown as a format only — always read the real ones from the VS-Dates wiki.\n\n### How to determine `PACKAGE_VALIDATION_BASELINE_VERSION`\n\n**The value is the latest `{{THIS_RELEASE_VERSION}}.0-<label>.<shortDate>.<rev>` MSBuild package that is both:**\n\n_`<label>` is `PreReleaseVersionLabel` from `eng/Versions.props` (currently `1`), so the versions look like `18.11.0-1.26426.2`. Do **not** assume the legacy `-preview-NNNNN-NN` shape — MSBuild no longer produces it._\n\n1. **Published on the public [dotnet-tools feed](https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-tools)** — this is the feed the official build publishes to and that ApiCompat restores baselines from. If the version isn't here, ApiCompat fails with `NU1102`.\n2. **Produced from a commit reachable from `vs{{THIS_RELEASE_VERSION}}`** — i.e. a commit on `vs{{THIS_RELEASE_VERSION}}`, or the `main` commit `vs{{THIS_RELEASE_VERSION}}` was branched from.\n\n**Two tempting wrong answers — and why they're wrong:**\n\n| Wrong pick | Why it fails |\n|---|---|\n| ❌ The release-versioned `{{THIS_RELEASE_VERSION}}.X` package that ships in VS / on nuget.org | Since [#14277](https://github.com/dotnet/msbuild/pull/14277) release branches build and insert **prerelease** versions, exactly like `main`; the release-versioned variants are produced by NuGetRepack at manual publish time. They never exist on the public CI feed, so ApiCompat cannot restore them. |\n| ❌ Blindly the most recent `{{THIS_RELEASE_VERSION}}.0-*` on `dotnet-tools` | After `vs{{THIS_RELEASE_VERSION}}` branches, `main` keeps producing `{{THIS_RELEASE_VERSION}}.0-*` until **this** main-bump PR merges — so the most recent feed entries may be `{{NEXT_VERSION}}`-content builds wearing `{{THIS_RELEASE_VERSION}}` branding. Picking one drifts the API baseline forward and silently hides real compat breaks. |\n\n**Timing caveat:** the branch-point build publishes to `dotnet-tools` asynchronously. If you branch and bump on the same day, the script may report the correct candidate as `[NOT on feed]` — wait for publication and re-run rather than substituting an older build.\n\n**Procedure:** run the helper — it does the whole resolution mechanically (requires `az login` with devdiv access):\n\n```\npwsh ./scripts/Get-PackageValidationBaseline.ps1 -ThisReleaseVersion {{THIS_RELEASE_VERSION}}\n# -> prints e.g. 18.11.0-1.26426.2\n```\n\nIt computes `git merge-base origin/main origin/vs{{THIS_RELEASE_VERSION}}`, finds the matching successful build in [pipeline 9434](https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434), derives the package version from the OfficialBuildId, and verifies it on the dotnet-tools feed. If it fails, read the script's own `.DESCRIPTION` header for the manual equivalent rather than reproducing it here.\n\n### Prerequisites\n- gh cli\n- az cli\n- darc cli — Arcade enforces a minimum version; if `darc` refuses with a \"below the minimum required version\" error, run `.\\eng\\common\\darc-init.ps1`\n\n## Phase Summary\n\n| Phase | Trigger | Key Actions |\n|---|---|---|\n| **0: Instantiate** | User-initiated | Validate inputs, create GitHub tracking issue |\n| **1: Branch & Prepare** | `BRANCH_SNAP_DATE` | Create `vs*` branch, DARC channel setup (batched PR), **audit all `vs*` branches for retirement** |\n| **2: DARC Subscription Updates** | Phase 1 branch exists (`vs*` created) | Retarget `main`-targeting subs + VMR backflow to next channel, retired-branch cleanup (batched PR), Arcade verify |\n| **3: Bump Main** | Phase 2 ingested and verified | Branding PR in `main` (`VersionPrefix` → next, ApiCompat baseline, refresh OptProf baseline) |\n| **4: Final Branding** | 7 days before `INSIDERS_SNAP_DATE` | Telemetry evidence checks, package API compatibility review, OptProf bootstrap, VS insertion before Insiders snap |\n| **5: Post-GA** | VS shipped (`VS_SHIP_DATE`) | Resolve the exact shipped version (SDK-coupled? SDK wins over VS `rel/stable`), nuget.org publish, docs, GitHub release, Change Waves Learn sync, retro |\n\n## DARC Batching\n\nDARC write commands push to the [maestro-configuration](https://dev.azure.com/dnceng/internal/_git/maestro-configuration) repo. Batch related changes into **one PR**:\n\n1. Choose a branch name like `release/msbuild-{{THIS_RELEASE_VERSION}}`\n2. Add `--configuration-branch <name> --no-pr` to every write command except the last\n3. Last command: use `--configuration-branch <name>` without `--no-pr` to create the PR\n4. Review the diff before merging; disable auto-complete if the PR must remain review-only.\n5. After merging, complete [DARC configuration PR verification](../../../documentation/release-checklist.md#darc-configuration-pr-verification) before dependent release steps.\n\nCheck existing channels and mappings before writing: the previous release normally pre-created the current release's mapping. Skip satisfied writes rather than constructing a redundant batch.\n\nRead-only commands (`get-default-channels`, `get-subscriptions`, `get-channel`) don't need these flags.\n\n**Non-interactive (`-q`).** `darc add-default-channel` / `add-subscription` prompt `y/n` when the target branch does not exist yet (e.g. pre-creating the `vs{{NEXT_VERSION}}` mapping in Phase 1.2c, or adding the new `vs{{THIS_RELEASE_VERSION}}` backflow in Phase 2). `darc delete-subscriptions` always prompts for confirmation. Console input is redirected in an agent session, so the prompt **fails or hangs the command** — always pass `-q` for these writes. Note the verb is `delete-subscriptions` (plural); `delete-subscription` does not exist.\n\n**Phase 2 — forward-flow rotation.** Move subscriptions targeting `main` (`dotnet/dotnet @ main`, `dotnet/fsharp @ main`) to the next channel so those consumers receive next-version builds. Keep VMR `release/*` targets, including `.NET-next` preview bands, on `VS {{THIS_RELEASE_VERSION}}` so the release branch continues to feed its SDK bands.\n\n**Phase 2 — VMR backflow rotation.** Backflow (`dotnet/dotnet → msbuild`, source-enabled) keeps each MSBuild branch's dependencies aligned with its SDK band. When the new `vs{{THIS_RELEASE_VERSION}}` is paired with an SDK band, repoint the `→ main` backflow to the **next** SDK band channel and add backflow from the **outgoing** band channel into the release branch. Skip this for a VS-only release. See checklist steps 2.2b / 2.3f / 2.3g.\n\n## Branch retirement (Phase 1.3)\n\nAudit every release branch to keep required SDK/VS servicing flows active and remove unused dependency automation.\n\n**Branch inventory:** [Maintained branches](../../../documentation/release.md#maintained-branches) is the static list. Update its branch rows, SDK/VS sources, and support deadlines when cutting or retiring branches; cross-check the live DARC mappings, subscriptions, and repository branches in checklist step 1.3a.\n\nCheck both lifecycles because either the SDK or Visual Studio can require a branch:\n\n| Lifecycle | Source |\n|---|---|\n| **SDK band** | [supported .NET versions table](https://learn.microsoft.com/dotnet/core/porting/versioning-sdk-msbuild-vs#supported-net-versions) — maps each feature band to its VS version and support end date |\n| **Visual Studio** | [VS Product Lifecycle and Servicing](https://learn.microsoft.com/visualstudio/releases/2026/servicing-vs) — annual releases and the LTSC table |\n\n**VS 2026 and later:** non-LTSC releases are serviced through Insiders and for one month after Stable release. A new Stable promotion ends VS support for the previous release. LTSC baselines receive one year of servicing after release; use the published LTSC table to identify them.\n\nUse published SDK and VS deadlines for lifecycle decisions, and DARC channels and subscriptions for dependency routing.\n\nApply the **combined rule**: retire a branch paired with an SDK band and a VS version only when both lifecycles agree it is out of support. Check these candidates for automation cleanup:\n\n| Signal | Meaning |\n|---|---|\n| Branch's `VS X.Y` channel has **no MSBuild outbound subscription** | No downstream consumes MSBuild builds from that branch. |\n| Default channel exists for a **branch that isn't in the repo** | Pure orphan — delete the mapping. |\n\n**Retirement cleanup (2.3e):** remove inbound subscriptions, unused MSBuild outbound subscriptions, and branch-to-channel associations, including disabled subscriptions. Preserve shared channels and flows still needed by other branches or producers; verify removals after merge and ingestion.\n\n## Executing a Phase\n\nWhen asked to execute a specific phase:\n\n1. Read the full phase from `documentation/release-checklist.md`\n2. Verify the trigger condition is met (previous phases completed, including ingestion and live verification of prerequisite DARC changes)\n3. Execute steps in order — respect sequential/parallel annotations\n4. For DARC commands: batch writes into one configuration PR per phase\n5. Record all output URLs in the tracking issue's artifact table\n6. Mark checkboxes as completed in the tracking issue\n7. In **Phase 4**, complete telemetry checkpoints 4.0 and 4.4a using the shared evidence procedure in `documentation/release-checklist.md`.\n8. In **Phase 4** (step 4.7): if `documentation/wiki/ChangeWaves.md` is changed for this release, update the public Learn page at https://learn.microsoft.com/visualstudio/msbuild/change-waves. Sync the Change Waves Learn page from `documentation/wiki/ChangeWaves.md` on the `vsXX.Y` branch that is live in VS Insiders / the latest preview SDK. PR goes to `MicrosoftDocs/visualstudio-docs-pr` (`docs/msbuild/change-waves.md`); example: https://github.com/MicrosoftDocs/visualstudio-docs-pr/pull/15662.\n\n## Key Files\n\n| File | Purpose |\n|---|---|\n| [`documentation/release-checklist.md`](../../../documentation/release-checklist.md) | **Operational checklist** — the source of truth |\n| [`documentation/release.md`](../../../documentation/release.md) | Maintained-branch inventory and release process |\n| [`documentation/wiki/ChangeWaves.md`](../../../documentation/wiki/ChangeWaves.md) | Source doc for the Learn page sync — always sync the `vsXX.Y` (Insiders/preview-SDK) copy, not `main` |\n| [MSBuild Change Waves Learn page](https://learn.microsoft.com/visualstudio/msbuild/change-waves) | Public docs target to [`MicrosoftDocs/visualstudio-docs-pr`](https://github.com/MicrosoftDocs/visualstudio-docs-pr) (`docs/msbuild/change-waves.md`) |\n| [`eng/Versions.props`](../../../eng/Versions.props) | `VersionPrefix`, `PackageValidationBaselineVersion`, `BootstrapSdkVersion` |\n| [`azure-pipelines/vs-insertion.yml`](../../../azure-pipelines/vs-insertion.yml) | VS insertion pipeline — `TargetBranch` parameter and `InsertTargetBranch` routing |\n| [`azure-pipelines/vs-insertion-experimental.yml`](../../../azure-pipelines/vs-insertion-experimental.yml) | Experimental insertion — `TargetBranch` parameter values |\n| [`scripts/Get-PackageValidationBaseline.ps1`](../../../scripts/Get-PackageValidationBaseline.ps1) | Phase 3.2 — resolves `PackageValidationBaselineVersion` deterministically (merge-base → pipeline 9434 → dotnet-tools feed) |\n| [`scripts/Get-LatestOptProfDrop.ps1`](../../../scripts/Get-LatestOptProfDrop.ps1) | Phase 3.3 — resolves the latest main OptProf drop (MSBuild-OptProf pipeline 17389) to refresh `OptProfBaselineDrop` in `.vsts-dotnet.yml` |\n| [`.vsts-dotnet.yml`](../../../.vsts-dotnet.yml) | Build pipeline entry point — `OptProfBaselineDrop` (hardcoded OptProf seed for new `vs*` branches) |\n| [`azure-pipelines/.vsts-dotnet-build-jobs.yml`](../../../azure-pipelines/.vsts-dotnet-build-jobs.yml) | Build jobs — `VisualStudio.ChannelName` is release-agnostic (`int.main`); no per-release edit |\n\n## Validation\n\nAfter completing all phases, verify:\n\n1. Branch `vs{{THIS_RELEASE_VERSION}}` exists and has final branding\n2. Main has `VersionPrefix` = `{{NEXT_VERSION}}.0`\n3. DARC: main → `VS {{NEXT_VERSION}}` channel, release branch → `VS {{THIS_RELEASE_VERSION}}` channel\n4. VS insertion PR merged\n5. Packages published to nuget.org\n6. GitHub release created with tag `v{{THIS_RELEASE_EXACT_VERSION}}`\n7. The Learn page https://learn.microsoft.com/visualstudio/msbuild/change-waves lists exactly the waves present in `vsXX.Y` (the version in VS Insiders / the latest released preview SDK), or the sync is explicitly tracked\n\n## Error Recovery\n\n- **Branch already exists**: Release was partially started — check the tracking issue for progress\n- **DARC channel already exists**: Safe to continue — `add-channel` is idempotent\n- **OptProf fails on first build**: Expected — that's why we use main's OptProf data as fallback\n- **DARC configuration PR conflicts**: Rebase the configuration branch on `production` and force-push\n\n## Major Version Releases\n\nIf `NEXT_VERSION` is a new major version (e.g., 18.x → 19.0), additional steps are needed after Phase 5. See [release.md](../../../documentation/release.md) for:\n- `src/Shared/BuildEnvironmentHelper.cs` — VS major version constants\n- `src/Shared/Constants.cs` — version constants\n- `src/Framework/Telemetry/TelemetryConstants.cs` — telemetry version\n",".github/skills/reviewing-msbuild-code/SKILL.md":"---\nname: reviewing-msbuild-code\ndescription: \"Reviews MSBuild code changes using a 24-dimension methodology. Activates for code review, PR review, pull request analysis, design review, architecture review, code quality assessment, or style check of MSBuild code. Covers backwards compatibility, ChangeWave discipline, performance, allocation awareness, test coverage, error message quality, logging, string comparison, API surface, target authoring, cross-platform correctness, code simplification, concurrency, naming, SDK integration, evaluation model integrity, correctness, dependency management, security, and build infrastructure.\"\n---\n\n# MSBuild Code Review\n\nInvoke `@expert-reviewer` for 24-dimension MSBuild code review. The agent contains the full methodology, principles, dimension rules with checklists, folder hotspot mapping, and 4-wave workflow.\n",".github/skills/running-unit-tests/SKILL.md":"---\nname: running-unit-tests\ndescription: Guide for running MSBuild unit tests efficiently. Use when running, scoping, filtering, or speeding up unit tests in this repository, or when finalizing a change with a heavier validation pass. Covers xUnit v3 + Microsoft.Testing.Platform (MTP) specifics and which `dotnet test` flags do and don't apply.\nargument-hint: Run, filter, scope, or speed up MSBuild unit tests.\n---\n\n# Running MSBuild Unit Tests\n\nThis repo uses **xUnit v3** with the **Microsoft.Testing.Platform (MTP)** runner. Test projects are built as `OutputType=Exe`, so each test assembly is a self-contained host process — *not* a classic VSTest assembly.\n\nThere are **two ways to run tests**:\n\n| Method | When to use |\n|--------|-------------|\n| `dotnet test <project>` | Dev loop — run a single test project, optionally filtered |\n| `build.cmd -test` / `build.sh --test` | Final validation — builds everything and runs all test projects via the repo's Arcade harness |\n\nUse a **fast, scoped** `dotnet test` loop while iterating, and the **full `build.cmd -test`** pass before declaring the change done.\n\n## Repo-specific knobs to know about\n\nThese are configured in `Directory.Build.props` (repo root), `src/Directory.Build.targets`, and `src/Shared/UnitTests/xunit.runner.json`. They apply to both `dotnet test` and `build.cmd -test` unless noted otherwise:\n\n- **Multi-targeting**: test projects target .NETFramework and .NETCoreApp on Windows (.NETCoreApp only on Linux/macOS). `dotnet test` runs the suite **once per TFM**.\n- **Single-threaded by default**: `xunit.runner.json` sets `maxParallelThreads: 1` and `parallelizeTestCollections: false`. Many tests mutate process-global state (env vars, cwd, SDK resolvers), so this is intentional.\n- **Auto trait filters**: platform/TFM-inappropriate tests are filtered out via `--filter-not-trait Category=...` (e.g., `nonwindowstests`, `failing`, `nonnetcoreapptests`). Don't try to \"fix\" tests that appear skipped because of these.\n- **Coverage on non-Windows**: `--coverage --coverage-settings Coverage.config` is appended unconditionally to `XunitOptions` in `src/Directory.Build.targets`. There is no MSBuild property switch to disable it from `dotnet test` — to skip coverage, run the test exe directly without `--coverage`.\n- **Test runner**: `TestRunnerName=XUnitV3`, MTP `1.9.1`, xUnit v3 `3.2.2` (set in repo-root `Directory.Build.props`).\n- **DOTNET_HOST_PATH**: `RunnerUtilities.GetMSBuildEnvironmentVariables` (in `src/UnitTests.Shared/RunnerUtilities.cs`) sets `DOTNET_HOST_PATH` to the bootstrap dotnet when tests launch MSBuild as a child process, so tasks like `RoslynCodeTaskFactory` resolve the right host. Don't override `DOTNET_HOST_PATH` from a test.\n\n## Flags — MTP vs VSTest pitfalls\n\nThis repo uses MTP with the xUnit v3 MTP runner, **not** VSTest. Many familiar `dotnet test` flags silently do nothing. Key differences:\n\n- **Use** `--report-trx` (not `--logger trx`), `--coverage` (not `--collect \"XPlat Code Coverage\"`), `--filter-method`/`--filter-class`/`--filter-trait` for xUnit v3 native filtering.\n- **Don't use** `--filter`, `--nologo`, `--blame`, `--settings *.runsettings`, `--diag`, `--collect`, or `-- RunConfiguration.MaxCpuCount=...` — these are VSTest-only and ignored.\n- **Still works**: `-c`, `-f`, `--no-restore`, `--no-build`, `-v`, `-bl:`, `-p:Property=Value` — these are interpreted by `dotnet test` itself before MTP sees them.\n\nFor comprehensive MTP vs VSTest flag reference, see the `run-tests` skill from the `dotnet-test` plugin.\n\n## Dev loop: fast and scoped\n\nAim for sub-30s iterations.\n\n> Examples below use Windows-style backslashes and PowerShell line continuations. Forward slashes work everywhere with `dotnet` (`src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj`); on Linux/macOS use `/` and shell line continuations (`\\`), and the exe path becomes `artifacts/bin/<Proj>/Debug/net11.0/<Proj>` (no `.exe`).\n\n1. **Run via `dotnet test` with a single TFM and filter** (incremental build handles rebuilding automatically):\n   ```powershell\n   dotnet test src\\Tasks.UnitTests\\Microsoft.Build.Tasks.UnitTests.csproj `\n     -f net11.0 -- --filter-method \"*MyFeature*\"\n   ```\n2. **Or run the test exe directly** (fastest — no SDK overhead; build first if source changed):\n   ```powershell\n   dotnet build src\\Tasks.UnitTests\\Microsoft.Build.Tasks.UnitTests.csproj -c Debug -f net11.0\n   artifacts\\bin\\Microsoft.Build.Tasks.UnitTests\\Debug\\net11.0\\Microsoft.Build.Tasks.UnitTests.exe `\n     --filter-method \"*MyFeature*\" --no-progress\n   ```\n   The exe path is TFM-specific: `Debug\\net11.0\\...exe` for net11.0, `Debug\\net472\\...exe` for net472. Switching TFM without rebuilding silently runs stale binaries.\n\n### Speeding up the dev loop further\n\nThese trade safety for speed — use during iteration, **revert before final validation**:\n\n-- **Single TFM**: pass `-f net11.0`. Halves runtime on Windows by skipping `net472`.\n- **Temporarily relax single-threaded execution**: drop a `xunit.runner.json` next to the test project (or override the existing one) with:\n  ```json\n  {\n    \"$schema\": \"https://xunit.net/schema/current/xunit.runner.schema.json\",\n    \"longRunningTestSeconds\": 60,\n    \"maxParallelThreads\": -1,\n    \"parallelizeTestCollections\": true\n  }\n  ```\n  Or pass them as runner args after `--`: `-- xUnit.MaxParallelThreads=-1 xUnit.ParallelizeTestCollections=true`. Expect flakes in tests that touch env vars, cwd, the file system, or the global ProjectCollection — **don't ship a fix that depends on this being on**.\n- **Skip bootstrap packaging** for non-bootstrap test projects:\n  `dotnet build ... -p:CreateBootstrap=false` (useful when the local bootstrap SDK payload is missing).\n- **Narrow with traits**: `--filter-trait Category=mytraitduringdev` if you've tagged a focused subset.\n- **Skip code coverage on Linux/macOS**: run the test exe directly without `--coverage`. This repo does not expose a supported `dotnet test` property switch to disable the auto-added coverage arguments.\n\n## Final validation pass\n\nBefore saying \"done,\" run the heavy configuration. Don't skip TFMs and don't keep parallel-overrides.\n\n1. **Restore parallelism settings** (revert any local `xunit.runner.json` change).\n2. **Run all TFMs for affected projects**:\n   ```powershell\n   dotnet test src\\Build.UnitTests\\Microsoft.Build.Engine.UnitTests.csproj -c Release\n   dotnet test src\\Tasks.UnitTests\\Microsoft.Build.Tasks.UnitTests.csproj -c Release\n   # Add other UnitTests projects whose code paths you touched\n   ```\n3. **For broad changes (engine, framework, shared)**, run the full repo test suite (~9 minutes — do not cancel):\n   ```powershell\n   .\\build.cmd -test -c Release       # Windows\n   ./build.sh --test -c Release       # Linux/macOS\n   ```\n4. **Capture a TRX** if reporting results to the user or CI:\n   ```powershell\n   dotnet test <project> -c Release `\n     --report-trx --report-trx-filename validation.trx `\n     --results-directory artifacts\\TestResults\n   ```\n\n## Reading the summary line\n\nMTP's final summary reports `passed`, `failed`, and `skipped` separately. **Always check the `skipped` count** — platform-conditional attributes (`WindowsOnlyFact`, `UnixOnlyFact`, etc.) and the auto trait filters cause expected skips, but a sudden jump in skipped count can hide a regression where a test became inapplicable on the current platform without you intending it.\n\n## Picking the right project to run\n\nMatch the source area you changed to its `*.UnitTests` project:\n\n| Source area | Test project |\n|-------------|--------------|\n| `src/Build/**` (engine, evaluation, backend) | `src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj` |\n| `src/Framework/**` | `src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj` |\n| `src/Tasks/**` | `src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj` |\n| `src/Utilities/**` | `src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj` |\n| `src/MSBuild/**` (CLI) | `src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj` |\n| `src/Build/BuildCheck/**` | `src/BuildCheck.UnitTests/Microsoft.Build.BuildCheck.UnitTests.csproj` |\n| `src/Shared/**` | Run the consumers above (Build, Tasks, Utilities) — shared code is linked into all of them. |\n\n## Quick reference\n\n| Scenario | Command |\n|----------|---------|\n| Fast scoped dev loop | `dotnet test <proj> -f net11.0 -- --filter-method \"*X*\"` |\n| Direct test exe | `artifacts\\bin\\<Proj>\\Debug\\net11.0\\<Proj>.exe --filter-method \"*X*\" --no-progress` |\n| Single test by name | `dotnet test <proj> -- --filter-method \"*MyTestMethod*\"` |\n| Final per-project validation | `dotnet test <proj> -c Release` (all TFMs) |\n| Final full validation | `.\\build.cmd -test -c Release` / `./build.sh --test -c Release` |\n| TRX report | `--report-trx --report-trx-filename out.trx --results-directory artifacts\\TestResults` |\n\n## See also\n\n- `.github/instructions/tests.instructions.md` — test authoring conventions (xUnit v3, Shouldly, `TestEnvironment`, `MockLogger`).\n- `src/Shared/UnitTests/xunit.runner.json` — repo-wide xUnit settings.\n- `src/Directory.Build.targets` — `XunitOptions`, auto trait filters, coverage wiring.\n- `documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md`\n",".github/skills/use-bootstrap-msbuild/SKILL.md":"---\nname: use-bootstrap-msbuild\ndescription: Guide for testing bug reproductions against locally-built MSBuild. Use this when you have a repro project and want to verify a fix works before submitting a PR.\n---\n\n# Testing Bug Reproductions with Bootstrap MSBuild\n\nThis skill guides you through testing a bug reproduction project against your locally-built MSBuild to verify a fix.\n\n## Overview\n\nAfter making changes to MSBuild, you need to test them against a repro project. The \"bootstrap\" is a self-contained MSBuild installation built from your local changes. It includes all dependencies needed to build real projects.\n\n## Step 1: Build MSBuild with Bootstrap\n\nBuild MSBuild to create the bootstrap directory:\n\n```powershell\n# Windows\n.\\build.cmd\n\n# Unix/macOS\n./build.sh\n```\n\nThis creates the bootstrap at `artifacts\\bin\\bootstrap\\` with your changes.\n\nRerun this after making any code change. If run in the default mode, there may be some errors on subsequent builds about locked files (due to MSBuild worker node processes lingering). If so, run `./artifacts/bin/bootstrap/core/dotnet build-server shutdown`.\n\n## Step 2: Run Your Repro Project\n\n### .NET Core / .NET SDK Projects\n\nUse the bootstrap `dotnet` CLI directly (preferred):\n\n```powershell\n# Windows\nartifacts\\bin\\bootstrap\\core\\dotnet.exe build <path-to-repro.csproj>\n```\n\n```bash\n# Unix/macOS\n./artifacts/bin/bootstrap/core/dotnet build <path-to-repro.csproj>\n```\n\nAll of the usual command line arguments should work, including `-bl` to create binlogs.\n\n### .NET Framework Projects\n\nIf the problem is specific to the .NET Framework `MSBuild.exe` that is used in Visual Studio, and you're running on Windows, you can use the bootstrap MSBuild.exe directly:\n\n```powershell\nartifacts\\bin\\bootstrap\\net472\\MSBuild\\Current\\Bin\\MSBuild.exe <path-to-repro.csproj>\n```\n\n**Note**: The .NET Framework bootstrap output will only be complete when built on Windows using `MSBuild.exe`.\n\n### Changes not reflected\n\n1. Verify bootstrap was rebuilt: check `artifacts\\bin\\bootstrap\\core\\sdk\\*\\MSBuild.dll` timestamp\n2. Kill any lingering MSBuild server processes:\n   ```powershell\n   ./artifacts/bin/bootstrap/core/dotnet build-server shutdown\n   # Or use the helper function after sourcing msbuild-build-env.ps1\n   killdotnet\n   ```\n\n### Repro works with bootstrap but not with installed MSBuild\n\nYour fix is working! The repro uses your local changes while the installed MSBuild has the bug.\n\n## Quick Reference\n\n| Scenario | Command |\n|----------|---------|\n| Build bootstrap | `.\\build.cmd` |\n| .NET Core repro | `artifacts\\bin\\bootstrap\\core\\dotnet.exe build <project>` |\n| .NET Framework repro | `artifacts\\bin\\bootstrap\\net472\\MSBuild\\Current\\Bin\\MSBuild.exe <project>` |\n\n## See Also\n\n- [Bootstrap Documentation](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Bootstrap.md)\n- [Building and Debugging Guide](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md)\n","AGENTS.md":"# Agent Instructions\n\nInstructions for GitHub Copilot and other AI coding agents working with the MSBuild repository.\n\n## Repository Overview\n\n**MSBuild** is the Microsoft Build Engine - performance-critical infrastructure for .NET and Visual Studio builds. This repository contains the code for the MSBuild build engine, including its public C# API, its internal implementation of the MSBuild programming language, and core targets and tasks used for builds.\n\n### Key Components\n- **Microsoft.Build**: Core MSBuild engine and public API\n- **Microsoft.Build.Framework**: Framework interfaces and base types\n- **Microsoft.Build.Tasks**: Built-in MSBuild tasks\n- **Microsoft.Build.Utilities**: Utility classes for task authors\n- **MSBuild CLI**: Command-line tool for invoking builds\n\n### Technology Stack\n- .NET 10.0 and .NET Framework 4.7.2\n- C# 14 features (especially collection expressions)\n- xUnit with Shouldly for testing\n- Multi-platform support (Windows, Linux, macOS)\n\n## General\n\n* Performance is the top priority - minimize allocations, avoid LINQ in hot paths, use efficient algorithms.\n* Always use the latest C# features, currently C# 14, especially collection expressions (`[]` over `new Type[]`).\n* Match the style of surrounding code when making edits, but modernize aggressively for substantial changes.\n\n## Code Review Instructions\n\nOfficial builds treat all warnings as errors, so do not introduce new warnings.\n\n### Performance Considerations\n\nWhen reviewing pull requests:\n\n* **Flag any unnecessary allocations** in hot paths\n* **Flag LINQ usage** in performance-critical code paths\n* **Check for proper use of `Span<T>`** and `ReadOnlySpan<T>` for string parsing\n* **Ensure immutable collections** use the correct type (`ImmutableArray<T>` and `FrozenDictionary<TKey, TValue>` for read-heavy, `ImmutableList<T>` for incremental building)\n\n### NuGet Feed Configuration\n\nWhen reviewing pull requests:\n\n* **Flag any changes to NuGet.config** that add external package sources without justification\n* Package sources should use approved internal feeds when possible\n\n## Formatting\n\n* Apply code-formatting style defined in `.editorconfig`.\n* Prefer file-scoped namespace declarations and single-line using directives.\n* Insert a newline before the opening curly brace of any code block.\n* Use pattern matching and switch expressions wherever possible.\n* Use `nameof` instead of string literals when referring to member names.\n\n### Nullable Reference Types\n\n* **New files**: Always use nullable reference types (do NOT add `#nullable disable`)\n* **Existing files with `#nullable disable`**: Match the existing style; don't add nullable annotations (`?`) to types\n* **Existing files with nullable enabled**: Use proper nullable annotations\n* Always use `is null` or `is not null` instead of `== null` or `!= null`\n\n## Performance Best Practices\n\n### Range Pattern Matching\n\n```csharp\n// GOOD: Clear and efficient\nreturn errorNumber switch\n{\n    >= 3001 and <= 3999 => Category.Tasks,\n    >= 4001 and <= 4099 => Category.General,\n    >= 4100 and <= 4199 => Category.Evaluation,\n    _ => Category.Other\n};\n```\n\n### String Handling\n\n* Use `MSBuildNameIgnoreCaseComparer` for case-insensitive comparisons of MSBuild names; use `StringComparer.OrdinalIgnoreCase` only for non-MSBuild string comparisons\n* Use `char.ToUpperInvariant()` for single-character comparisons\n* Use `ReadOnlySpan<char>` and `Slice()` to avoid string allocations\n* Use `int.TryParse(span, out var result)` on .NET Core+ for allocation-free parsing\n\n### Inlining Hot Paths\n\n```csharp\n[MethodImpl(MethodImplOptions.AggressiveInlining)]\nprivate static bool IsCompilerPrefix(string value) => ...\n```\n\n### Immutable Collections\n\n**Build once, read many times** (most common in MSBuild):\n```csharp\nImmutableArray<string> items = source.Select(x => x.Name).ToImmutableArray();\nFrozenDictionary<string, int> lookup = pairs.ToFrozenDictionary(x => x.Key, x => x.Value);\n```\n\n**Build incrementally over time**:\n```csharp\nImmutableList<string> items = ...;  // Use when adding items one by one\nImmutableDictionary<string, int> lookup = ...;\n```\n\n## Building\n\nMost development can use `dotnet build`. For the most complete possible build, use `MSBuild.exe` from Visual Studio 2026. See the [Windows build prerequisites](README.md#building-msbuild-with-visual-studio-2026-on-windows).\n\nNEVER pipe MSBuild output to a file. If you want a list of errors, add `-flp:\"v=q;LogFile=ErrorsAndWarnings.log\"` to the arguments.\n\nWhen considering a subset of build errors, always look at the BEGINNING of the set, not the end.\n\n## Individual projects\n\nBuild individual projects with `dotnet msbuild {path/to/project.csproj} -v:q`.\n\n### Whole-repo Build Commands\n\n| Platform | Command | Timeout |\n|----------|---------|---------|\n| Windows | `.\\build.cmd -v quiet` | 300+ seconds (~2-3 minutes) |\n| macOS/Linux | `./build.sh -v quiet` | 300+ seconds (~2-3 minutes) |\n\n### Bootstrap Environment Setup\n\nAfter building the whole repo, to use the just-built MSBuild to build things use the bootstrap environment (described in [`skills/use-bootstrap-msbuild](.github/skills/use-bootstrap-msbuild/SKILL.md)).\n\n### Build Troubleshooting\n\n* If build fails with \"Could not resolve SDK\", run the bootstrap environment script\n* Verify `dotnet --version` shows the preview/internal version\n* Use repository sample projects for testing, not external projects\n* Build artifacts go to `./artifacts/` directory\n\n### Running Tests\n\n**Windows:**\n```cmd\n# Full test suite (~9 minutes) - NEVER CANCEL\n.\\build.cmd -test\n\n# Individual test project (recommended):\ndotnet test src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj\n```\n\n**macOS/Linux:**\n```bash\n# Full test suite (~9 minutes) - NEVER CANCEL\n./build.sh --test\n\n# Individual test project (recommended):\ndotnet test src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj\n```\n\nYou can run a single unit test with `-- --filter-method \"*{methodname}*\"`, for example `dotnet test src/Framework.UnitTests/ -- --filter-method \"*ExerciseBuildEventContext*\"`.\n\n### Test Verification\n\n* **Individual Test Project**: ~10-60 seconds per project\n* **Full Test Suite**: ~9 minutes\n\n## Project Layout and Architecture\n\n### Directory Structure\n\n```\nsrc/\n├── Build/                    # Core MSBuild engine (Microsoft.Build)\n├── Build.UnitTests/          # Unit tests for core engine\n├── MSBuild/                  # MSBuild command-line tool\n├── Framework/                # MSBuild Framework (Microsoft.Build.Framework)\n├── Framework.UnitTests/      # Unit tests for framework\n├── Tasks/                    # Built-in MSBuild tasks (Microsoft.Build.Tasks)\n├── Tasks.UnitTests/          # Unit tests for tasks\n├── Utilities/                # MSBuild utilities (Microsoft.Build.Utilities)\n├── Utilities.UnitTests/      # Unit tests for utilities\n├── Shared/                   # Shared code across assemblies\n└── Samples/                  # Sample projects and extensions\n\nartifacts/\n├── bin/                      # Built binaries\n│   └── bootstrap/\n│       └── core/\n│           └── MSBuild.dll   # Built MSBuild executable\n├── sdk-build-env.sh          # Bootstrap script (Linux/macOS)\n├── msbuild-build-env.bat     # Bootstrap script (Windows)\n└── packages/                 # Built NuGet packages\n\ndocumentation/\n├── wiki/                     # Developer documentation\n├── specs/                    # Technical specifications\n└── *.md                      # Various documentation files\n```\n\n### Key Configuration Files\n\n* **`global.json`**: Pins .NET SDK version - never modify without explicit request\n* **`NuGet.config`**: Package source configuration - never modify without explicit request\n* **`.editorconfig`**: Code formatting rules\n* **`Directory.Build.props`**: Shared MSBuild properties across all projects\n* **`Directory.Packages.props`**: Centralized package version management\n* **`MSBuild.slnx`**: Main solution file\n\n## Validation Checklist\n\nBefore completing any change:\n\n1. ✅ Full build completes successfully (`.\\build.cmd` or `./build.sh`)\n2. ✅ Bootstrap environment activates correctly (`dotnet --version` shows preview)\n3. ✅ Sample project builds: `dotnet build src/Samples/Dependency/Dependency.csproj`\n4. ✅ Relevant unit tests pass\n5. ✅ `dotnet artifacts/bin/bootstrap/core/MSBuild.dll --help` works\n\n## Do NOT Modify\n\n* `artifacts/` directory contents - Generated during build\n* `.dotnet/` directory contents - Local SDK location\n\nSee **Key Configuration Files** section for files that should not be modified without explicit request.\n\n## Documentation\n\nWhen making changes, check if related documentation exists in the `documentation/` folder (including `documentation/specs/`) and update it to reflect your changes. Keep documentation in sync with code changes.\n\n## Breaking Changes\n\nBecause MSBuild is a critical part of the build process for a huge number of customers, we avoid breaking changes. Adding new errors or warnings, even when well-intentioned and pointing out things that are very likely to be wrong, is an unacceptable breaking change. Adding warnings is a breaking change because many production builds use `/WarnAsError`.\n\nThe exception to this policy is in new, opt-in behavior. In new, opt-in functionality, liberally emit warnings and errors--they can always be removed later.\n\nWhen reviewing PRs, always consider whether the behavior change could be experienced as a break in existing builds and flag any new warnings or errors.\n\n## Development Workflow\n\n1. Make your changes to source code\n2. Run the full build (WAIT for completion - takes 2-3 minutes):\n   - Windows: `.\\build.cmd -v quiet`\n   - macOS/Linux: `./build.sh -v quiet`\n4. Test your changes end-to-end using the bootstrap output: `dotnet build src/Samples/Dependency/Dependency.csproj`\n5. Run relevant individual tests, not the full test suite\n6. Commit your changes\n\n## Trust These Instructions\n\nThese instructions are comprehensive and tested. Only search for additional information if:\n1. The instructions appear outdated or incorrect\n2. You encounter specific errors not covered here\n3. You need details about new features not yet documented\n\nFor most development tasks, following these instructions should be sufficient to build, test, and validate changes successfully.\n\n## Updating these instructions\n\nWhen working on a task, if user input is required to complete the task or feedback is provided for guidance around a specific area of the code, evaluate that feedback/guidance and update this document to incorporate that feedback if it's missing.  This document should be a live, evolving set of instructions.\n","plugins/mt-migration/README.md":"# mt-migration plugin\n\nA GitHub Copilot plugin that bundles the MSBuild **multithreaded task migration** playbook plus an MT-specific PR reviewer.\n\nUse this on any repository that authors MSBuild tasks consuming the\n`IMultiThreadableTask` / `TaskEnvironment` / `[MSBuildMultiThreadableTask]` API\nsurface from `Microsoft.Build.Framework` — currently `dotnet/msbuild`,\n`dotnet/sdk`, and downstream task assemblies (NuGet, EF, ASP.NET Core, etc.).\n\n## Contents\n\n```\nmt-migration/\n├── plugin.json\n├── skills/\n│   └── multithreaded-task-migration/\n│       └── SKILL.md         # The 8 deadly sins, ToolTask hazards, helper patterns, test patterns, sign-off checklist\n└── agents/\n    └── mt-migration-reviewer.agent.md   # MT-specific PR reviewer that follows call chains end-to-end\n```\n\n## What it does\n\n| Component | When invoked | What it does |\n|---|---|---|\n| `multithreaded-task-migration` skill | Author migrating a task | Step-by-step migration recipe + compatibility red-team checklist. Distilled from ~50 merged migration PRs across `dotnet/msbuild` and `dotnet/sdk`. |\n| `mt-migration-reviewer` agent | Reviewing an MT migration PR | Delegates the 24-dimension general review to the host repo's expert reviewer (if any), then layers MT-specific findings on top. Mandatorily traces every call chain from `Execute()` to leaves and reports hazards by chain, not by file. Verifies tests are not theater. |\n\nThe reviewer is deliberately complementary to a generic code reviewer: it does\nnot re-explain migration steps, does not redo style/perf/naming review, and\nwill not flag a clean attribute-only migration as needing a concurrency test if\nthe call-chain audit comes back clean.\n\n## Installing\n\n### As a local plugin (from a clone of this repo)\n\n```sh\n# from a GitHub Copilot session\n/plugin install file://$(pwd)/plugins/mt-migration\n```\n\n### Across multiple repos (recommended)\n\nPublish this plugin folder to a small standalone repo (e.g.\n`your-org/mt-migration-plugin`) and install from there. The plugin contains no\nhard-coded references to `dotnet/msbuild` repo-local filesystem paths — only to\nthe public `Microsoft.Build.Framework` API surface (with source links for\nreference) — so it works on any task-authoring repo unchanged.\n\n```sh\n/plugin install github:your-org/mt-migration-plugin\n```\n\n## Relationship to the in-repo skill\n\nThis plugin is the **single, canonical** home of the MT migration skill. It is not duplicated under `.github/skills/` in `dotnet/msbuild` — install the plugin to use it locally. The skill-validator workflow in `dotnet/msbuild` only scans `.github/skills/` and `.github/agents/`, so the plugin lives outside that scope and is not subject to it.\n\n## Updating the skill from PR experience\n\nThe skill's \"deadly sins\" and call-chain hazard table grow as new\ndefect classes are found in merged migrations. To contribute:\n\n1. Find a defect that was caught in PR review and would have been missed by\n   the existing skill.\n2. Add a sin / hazard / pattern with a real-world citation (PR + line range).\n3. Update `skills/multithreaded-task-migration/SKILL.md`.\n4. Keep the agent file slim — it should keep delegating to the skill rather\n   than absorbing the rules itself.\n","plugins/mt-migration/plugin.json":"{\n  \"name\": \"mt-migration\",\n  \"version\": \"0.1.0\",\n  \"description\": \"MSBuild multithreaded task migration toolkit: the migration playbook + an MT-specific code reviewer that traces call chains end-to-end. Portable across repos that consume Microsoft.Build.Framework's TaskEnvironment/IMultiThreadableTask API (e.g., dotnet/msbuild, dotnet/sdk, and any task assembly migrating to the MT model).\",\n  \"skills\": [\"./skills/\"],\n  \"agents\": [\n    \"./agents/mt-migration-reviewer.agent.md\"\n  ]\n}\n","plugins/mt-migration/skills/multithreaded-task-migration/SKILL.md":"---\nname: multithreaded-task-migration\ndescription: Guide for migrating MSBuild tasks to multithreaded mode support, including compatibility red-team review. Use this when converting tasks to thread-safe versions, implementing IMultiThreadableTask, adding TaskEnvironment support, or auditing migrations for behavioral compatibility.\n---\n\n# Migrating MSBuild Tasks to Multithreaded API\n\nMSBuild's multithreaded execution model requires tasks to avoid global process state (working directory, environment variables). Thread-safe tasks declare this capability via `MSBuildMultiThreadableTask` and use `TaskEnvironment` from `IMultiThreadableTask` for safe alternatives.\n\n## Migration Steps\n\n### Step 1: Update Task Class Declaration\n\na. Ensure the task implementing class is decorated with the `MSBuildMultiThreadableTask` attribute.\nb. Implement `IMultiThreadableTask` **only if** the task needs `TaskEnvironment` APIs (path absolutization, env vars, process start). If the task has no file/environment operations (e.g., a stub class), the attribute alone is sufficient.\n\n```csharp\n[MSBuildMultiThreadableTask]\npublic class MyTask : Task, IMultiThreadableTask\n{\n    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;\n    ...\n}\n```\n\n**Note**: `[MSBuildMultiThreadableTask]` has `Inherited = false` — it must be on each concrete class, not just the base. The corollary is easy to miss: the base class still executes multithreaded, but is not analyzed. Audit the whole base chain — see [Unsafe Code in an Unannotated Base Class](#unsafe-code-in-an-unannotated-base-class).\n\n### Step 2: Absolutize Paths Before File Operations\n\nAll path strings must be absolutized with `TaskEnvironment.GetAbsolutePath()` before use in file system APIs. This resolves paths relative to the project directory, not the process working directory.\n\n```csharp\nAbsolutePath absolutePath = TaskEnvironment.GetAbsolutePath(inputPath);\nif (File.Exists(absolutePath))\n{\n    string content = File.ReadAllText(absolutePath);\n}\n```\n\nThe [`AbsolutePath`](https://github.com/dotnet/msbuild/blob/main/src/Framework/PathHelpers/AbsolutePath.cs) struct:\n- `Value` — the absolute path string\n- `OriginalValue` — preserves the input path (use for error messages and `[Output]` properties)\n- Implicitly convertible to `string` for File/Directory API compatibility\n- `GetCanonicalForm()` — resolves `..` segments and normalizes separators (see [Sin 5](#sin-5-canonicalization-mismatch))\n\n**CAUTION**: `GetAbsolutePath()` throws `ArgumentException` for null/empty inputs. See [Sin 3](#sin-3-null-coalescing-that-changes-control-flow) and [Sin 6](#sin-6-exception-type-change) for compatibility implications.\n\n### Step 3: Replace Environment Variable APIs\n\n| BEFORE (UNSAFE)                                  | AFTER (SAFE)                                       |\n|--------------------------------------------------|----------------------------------------------------|\n| `Environment.GetEnvironmentVariable(\"VAR\");`       | `TaskEnvironment.GetEnvironmentVariable(\"VAR\");`     |\n| `Environment.SetEnvironmentVariable(\"VAR\", \"v\");`  | `TaskEnvironment.SetEnvironmentVariable(\"VAR\", \"v\");` |\n\n### Step 4: Replace Process Start APIs\n\n| BEFORE (UNSAFE - inherits process state)           | AFTER (SAFE - uses task's isolated environment)     |\n|----------------------------------------------------|-----------------------------------------------------|\n| `var psi = new ProcessStartInfo(\"tool.exe\");`      | `var psi = TaskEnvironment.GetProcessStartInfo();`  |\n|                                                    | `psi.FileName = GetFullPathToTool(); // must be absolute` |\n\n## Updating Unit Tests\n\nBuilt-in MSBuild tasks now initialize `TaskEnvironment` with a `MultiProcessTaskEnvironmentDriver`-backed default. Tests creating instances of built-in tasks no longer need manual `TaskEnvironment` setup. For custom or third-party tasks that implement `IMultiThreadableTask` without a default initializer, set `TaskEnvironment = TaskEnvironment.Fallback` (or use `TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(path)` to point at a specific project directory).\n\n## APIs to Avoid\n\n| Category | APIs | Alternative |\n|---|---|---|\n| **Forbidden** | `Environment.Exit`, `FailFast`, `Process.Kill`, `ThreadPool.SetMin/MaxThreads`, `Console.*` | Return false, throw, or use `Log` |\n| **Use TaskEnvironment** | `Environment.CurrentDirectory`, `Get/SetEnvironmentVariable`, `Path.GetFullPath`, `ProcessStartInfo` | See Steps 2-4 |\n| **Need absolute paths** | `File.*`, `Directory.*`, `FileInfo`, `DirectoryInfo`, `FileStream`, `StreamReader/Writer` | Absolutize first (File System APIs) |\n| **Need absolute paths (analyzer-invisible)** | `AssemblyName.GetAssemblyName`, `XDocument/XElement/XmlDocument.Load(string)`, `XmlReader/XmlWriter.Create(string)`, `ZipFile.*`, `X509CertificateLoader`, `Image.FromFile`, `Assembly.LoadFrom` | Absolutize first — **the analyzer does not flag these** |\n| **Review required** | `Assembly.Load*`, `Activator.CreateInstance*` | Check for version conflicts |\n\n### Analyzer-Invisible Path Consumers\n\n`MSBuildTask0003` monitors a fixed list of types (`File`, `Directory`, `FileInfo`, `DirectoryInfo`, `FileStream`, `StreamReader`, `StreamWriter`, `FileSystemWatcher`). Any *other* API that accepts a path string and touches disk is equally unsafe but produces **no diagnostic**.\n\nIn a ~150-task migration across dotnet/arcade, dotnet/source-build-assets and the dotnet/dotnet VMR, `AssemblyName.GetAssemblyName` on a raw input was the single most-repeated defect. Do not treat a clean analyzer run as evidence that path handling is complete — see [Verification](#verification-a-clean-analyzer-run-is-not-a-migration).\n\nNote the overload distinction: the *string* overloads are hazards; `XDocument.Load(stream)` and `new StreamReader(stream)` are fine, because the caller already resolved the path to open the stream.\n\n## Practical Notes\n\n### CRITICAL: Trace All Path String Usage\n\nTrace every path string through all method calls and assignments to find all places it flows into file system operations — including helper methods that may internally use File System APIs.\n\n1. Find every path string (e.g., `item.ItemSpec`, function parameters)\n2. Trace downstream through all method calls\n3. Absolutize BEFORE any code path that touches the file system\n4. Use `OriginalValue` for user-facing output (logs, errors) — see [Sin 2](#sin-2-errorlog-message-path-inflation)\n\n**Trace through abstractions, not just concrete calls.** A path can reach the file system without a single `System.IO` type appearing in the task. In `InstallDotNetTool` (dotnet/arcade), the `DestinationPath`, `DotnetPath` and `WorkingDirectory` inputs flow raw through `IFileSystem` and `ICommandFactory` interfaces — the task body looks completely clean, and the analyzer reports nothing. When a task input is passed to an interface or delegate, resolve it at the task boundary rather than hoping the implementation does.\n\n**Resolve once, at the boundary.** The recurring failure is not \"forgot to absolutize\" but \"absolutized at three of four use sites\". If a variable is going to be used as a path at all, convert it to `AbsolutePath` where it enters the task and keep it that way — see [Sin 8](#sin-8-swallowed-exceptions-hiding-an-unresolved-path).\n\n### Exception Handling in Batch Operations\n\nIn batch processing (iterating over files), `GetAbsolutePath()` throwing on one bad path aborts the entire batch. Match the original task's error semantics:\n\n```csharp\nbool success = true;\nforeach (ITaskItem item in SourceFiles)\n{\n    try\n    {\n        AbsolutePath path = TaskEnvironment.GetAbsolutePath(item.ItemSpec);\n        ProcessFile(path);\n    }\n    catch (ArgumentException ex)\n    {\n        Log.LogError(\"Invalid path '{0}': {1}\", item.ItemSpec, ex.Message);\n        success = false;\n    }\n}\nreturn success;\n```\n\n### Prefer AbsolutePath Over String\n\nStay in the `AbsolutePath` world — it's implicitly convertible to `string` where needed. Avoid round-tripping through `string` and back.\n\n### TaskEnvironment is Not Thread-Safe\n\nIf your task spawns multiple threads internally, synchronize access to `TaskEnvironment`. Each task *instance* gets its own environment, so no synchronization between tasks is needed.\n\n### API Discipline When Plumbing Through Helpers\n\nWhen the migration ripples into shared helpers:\n\n- **Helper signatures: take `AbsolutePath`, not `(string path, string pathForMessages)`**. Two-string signatures drift apart; the caller passing one `AbsolutePath` keeps `.Value` and `.OriginalValue` in lockstep.\n- **Repo-wide helpers belong in an extensions class** (e.g., `TaskEnvironmentExtensions`), not on the task. If a per-task helper looks generic, extract it.\n- **Don't condition behavior on MT-mode**. `TaskEnvironment.Fallback` already gives single-process semantics; `if (mtMode) { … } else { … }` doubles the maintenance surface and skips test coverage of one branch.\n- **Don't silently swallow `ArgumentException` from `GetAbsolutePath`** in a helper — log a diagnostic so customers can debug bad inputs.\n\n## References\n\n- [Thread-Safe Tasks Spec](https://github.com/dotnet/msbuild/blob/main/documentation/specs/multithreading/thread-safe-tasks.md)\n- [`AbsolutePath`](https://github.com/dotnet/msbuild/blob/main/src/Framework/PathHelpers/AbsolutePath.cs)\n- [`TaskEnvironment`](https://github.com/dotnet/msbuild/blob/main/src/Framework/TaskEnvironment.cs)\n- [`IMultiThreadableTask`](https://github.com/dotnet/msbuild/blob/main/src/Framework/IMultiThreadableTask.cs)\n\n---\n\n# Compatibility Red-Team Playbook\n\nAfter migration, review for behavioral compatibility. **Every observable difference is a bug until proven otherwise.**\n\nObservable behavior = `Execute()` return value, `[Output]` property values, error/warning message content, exception types, files written, and which code path runs.\n\n## The 8 Deadly Compatibility Sins\n\nReal bugs found during MSBuild task migrations. Every one shipped in initial \"passing\" code with green tests.\n\n**Edge-case discipline:** For every migrated code path, verify behavior when inputs are `null`, empty string (`\"\"`), or whitespace-only. `GetAbsolutePath` throws `ArgumentException` on null/empty — if the pre-migration code handled these differently (e.g., returned early, used a default, or threw a different exception type), the migration must preserve that behavior. Even if a scenario seems unlikely, treat it as a relevant finding if it is theoretically possible.\n\n### Sin 1: Output Property Contamination\n\nAbsolutized values leak into `[Output]` properties that users/other tasks consume.\n\n```csharp\n// BROKEN: ManifestPath was \"bin\\Release\\app.manifest\", now \"C:\\repo\\bin\\Release\\app.manifest\"\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(Path.Combine(OutputDirectory, name));\nManifestPath = abs; // implicit string conversion!\n\n// CORRECT: separate original form from absolutized path\nstring originalPath = Path.Combine(OutputDirectory, name);\nAbsolutePath outputPath = TaskEnvironment.GetAbsolutePath(originalPath);\nManifestPath = originalPath;          // [Output]: original form\ndocument.Save((string)outputPath);    // file I/O: absolute path\n```\n\n**Detect**: For every `[Output]` property, trace backward — is it ever assigned from an `AbsolutePath`?\n\n### Sin 2: Error/Log Message Path Inflation\n\nError messages and exception messages show absolutized paths instead of the user's original input.\n\n**Direct leakage** — passing an `AbsolutePath` to logging APIs:\n\n```csharp\n// BROKEN: \"Cannot find 'C:\\repo\\app.manifest'\" instead of \"Cannot find 'app.manifest'\"\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(path);\nLog.LogError(\"Cannot find '{0}'\", abs); // implicit conversion!\n\n// CORRECT: use OriginalValue\nLog.LogError(\"Cannot find '{0}'\", abs.OriginalValue);\n```\n\n**Indirect leakage** — exception messages from helpers that received the absolutized path:\n\n```csharp\n// BROKEN: ex.FileName / ex.Message embed the absolutized path\ncatch (FileNotFoundException ex) { Log.LogError(\"Not found: {0}\", ex.FileName); }\ncatch (Exception ex)              { Log.LogError(ex.Message); }\n\n// CORRECT: prefer the original input; if you must use the exception, sanitize\ncatch (FileNotFoundException ex) { Log.LogError(\"Not found: {0}\", abs.OriginalValue); }\ncatch (Exception ex)              { Log.LogError(ex.Message.Replace(abs.Value, abs.OriginalValue)); }\n```\n\n**Detect**: Search every `Log.LogError`/`LogWarning`/`LogMessage` — is any argument an `AbsolutePath`? Also check every `Log.LogError(ex.Message …)` / `ex.FileName` downstream of a `GetAbsolutePath` — did the exception originate from a helper that received the absolutized path?\n\n### Sin 3: Null Coalescing That Changes Control Flow\n\nAdding `?? \"\"` silently swallows an exception the old code relied on for error handling.\n\n```csharp\n// BEFORE: Path.GetDirectoryName(\"C:\\\") → null → Path.Combine(null, x) → ArgumentNullException\n//   → task fails with an exception / error logged → Execute() returns false\n\n// BROKEN: ?? \"\" added \"for safety\"\nstring dir = Path.GetDirectoryName(fileName) ?? string.Empty;\n// Path.Combine(\"\", x) succeeds silently → no error → Execute() returns TRUE!\n```\n\n**Detect**: For every `??` you added, ask: \"What happened when this was null before?\" If it threw and was caught → your `??` is a bug.\n\n### Sin 4: Try-Catch Scope Mismatch\n\n`GetAbsolutePath()` inside a try block leaves the absolutized value out of scope in the catch block. Helper methods in the catch (like `LockCheck`) then use the original non-absolute path.\n\n```csharp\n// CORRECT: hoist above try so catch can use it too\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(OutputManifest.ItemSpec);\ntry {\n    WriteFile(abs);\n} catch (Exception ex) {\n    string lockMsg = LockCheck.GetLockedFileMessage(abs);        // absolute → correct file\n    Log.LogError(\"Failed: {0}\", OutputManifest.ItemSpec, ...);   // original → user-friendly\n}\n```\n\n**Detect**: For every `GetAbsolutePath` inside a try, check if the catch block needs the absolutized value.\n\n### Sin 5: Canonicalization Mismatch\n\n`GetAbsolutePath` does NOT canonicalize. `Path.GetFullPath` does TWO things: absolutize AND canonicalize (`..` resolution, separator normalization). If the old code used `Path.GetFullPath` for dictionary keys, comparisons, or display, you must add `.GetCanonicalForm()`:\n\n```csharp\n// GetAbsolutePath(\"foo/../bar\")  → \"C:\\repo\\foo/../bar\"  (NOT canonical)\n// Path.GetFullPath(\"foo/../bar\") → \"C:\\repo\\bar\"         (canonical)\n\n// BROKEN for dictionary keys — \"C:\\repo\\foo\\..\\bar\" ≠ \"C:\\repo\\bar\"\nvar map = items.ToDictionary(p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec), ...);\n\n// CORRECT\nvar map = items.ToDictionary(\n    p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec).GetCanonicalForm(),\n    StringComparer.OrdinalIgnoreCase);\n```\n\n**Detect**: Find every `Dictionary`/`HashSet`/`ToDictionary` using path keys, and every place the old code called `Path.GetFullPath`. If canonicalization mattered, add `.GetCanonicalForm()`.\n\n### Sin 6: Exception Type Change\n\nOld code threw `FileNotFoundException` for missing files; new code throws `ArgumentException` from `GetAbsolutePath(\"\")` before reaching the file check. Custom catch blocks filtering by exception type may be bypassed. (`ExceptionHandling.IsIoRelatedException` catches `ArgumentException`, but task-specific handlers might not.)\n\n**Detect**: For every `GetAbsolutePath`, check what the old code threw for null/empty and whether the calling code has type-specific catch blocks.\n\n### Sin 7: `Path.IsPathRooted` as an Absolutize Gate\n\n`Path.IsPathRooted` returns `true` for **drive-relative** (`C:foo\\bar`) and **root-relative** (`\\foo\\bar`) Windows paths — both still depend on process current-directory / current-drive state. \"Absolutize only if not rooted\" silently leaves those paths process-dependent.\n\n`GetAbsolutePath` correctly handles all path forms — including the Windows edge cases (`C:foo`, `\\foo`) that `Path.IsPathRooted` considers \"rooted\" but that are still CWD/drive-dependent. Call it unconditionally; remove any `IsPathRooted` short-circuit.\n\n### Sin 8: Swallowed Exceptions Hiding an Unresolved Path\n\nThe usual mental model is \"unresolved path → `FileNotFoundException` → loud failure\". That model is wrong wherever a `catch` treats an exception as a *semantic answer*. There, an unresolved path does not fail the build — it silently returns the wrong answer.\n\nReal example from the dotnet/dotnet VMR (`CheckForPoison`), where the same variable is resolved three lines later:\n\n```csharp\n// BROKEN\ntry\n{\n    AssemblyName asm = AssemblyName.GetAssemblyName(fileToCheck);   // raw, relative-capable\n    ...\n    if (IsAssemblyFromSbrp(TaskEnvironment.GetAbsolutePath(fileToCheck))) { ... }   // resolved\n    else if (IsAssemblyPoisoned(TaskEnvironment.GetAbsolutePath(fileToCheck))) { ... }\n}\ncatch (BadImageFormatException) { /* not a managed assembly - fine */ }\n```\n\nThe `GetAssemblyName` call is only an \"is this a managed assembly?\" probe, and the catch means \"not an assembly\". So an unresolved path throws, gets swallowed, and **both poison checks are skipped** — turning a leaked binary into a false negative in the leak-detection gate itself. The build stays green while the check it exists to perform quietly stops working.\n\nThis sin is especially dangerous because it defeats the usual verification strategy: the decoy-CWD test (Pattern A) still \"passes\" unless it asserts on the *result*, not just on `Execute()` returning true.\n\n**Detect**: For every `catch` in a migrated task, ask what the handler *means*. If it means anything other than \"fail\" — \"not an assembly\", \"not found, use default\", \"unsupported, skip\" — then every path-consuming call inside the corresponding `try` must be absolutized, and the catch should be narrowed to the exception types that genuinely represent that semantic answer (`BadImageFormatException` here, **not** `IOException`/`ArgumentException`).\n\n---\n\n## Call-Chain Hazards Beyond the Task\n\nThe 7 sins above are *local* to the task body. The other half of MT migration is auditing the **transitive call chain** — helpers and shared utility classes the task reaches into.\n\n### Helpers That Capture Process State\n\nHelpers reached from `Execute()` can quietly depend on process state in any of these ways:\n\n- A relative path falls through to `Directory.GetCurrentDirectory()` (often as a \"fallback\") or to `Path.GetFullPath(x)` without a base.\n- An env var is read directly via `Environment.GetEnvironmentVariable` (not `TaskEnvironment`).\n- A `static` field is seeded from process state on first use (`static string s_x = Directory.GetCurrentDirectory()`), permanently capturing the first caller's environment for every later caller.\n- A BCL API echoes its input path back through `ex.Message` / `ex.FileName` (Sin 2).\n- The helper takes a `string path` and returns a `string` — losing the `.OriginalValue` distinction, so callers either lie in messages or absolutize twice.\n\n**Resolution patterns:**\n\n1. Add an MT-aware overload that takes `TaskEnvironment` (or an `AbsolutePath` / explicit fallback path). Legacy overload delegates with `TaskEnvironment.Fallback`. Don't branch on \"MT mode on/off\".\n2. Promote the parameter type to `AbsolutePath` so `.Value` and `.OriginalValue` travel together.\n3. Replace process-state-seeded static caches with `ConcurrentDictionary<TKey, TValue>` keyed on the inputs that determine uniqueness — never on process state.\n4. For cross-repo helpers (foundation types used by many tasks), migrate the foundation in a dedicated PR before the tasks that consume it; the hydration step is usually where `Path.GetFullPath` is hiding.\n\n### Unsafe Code in an Unannotated Base Class\n\n`[MSBuildMultiThreadableTask]` is `Inherited = false`, so it goes on each concrete task. The consequence people miss is the mirror image: **the base class runs multithreaded too, but nothing marks it as such** — and with `msbuild_task_analyzer.scope = multithreadable_only` (the recommended setting for incremental migration) the analyzer does not look inside it at all.\n\nFrom dotnet/arcade: `CreateAkaMSLinks` and `DeleteAkaMSLinks` were both annotated, both analyzer-clean. Their shared base contained:\n\n```csharp\n// AkaMSLinksBase - NOT annotated, so never analyzed\nFile.ReadAllText(ClientCertificate)   // ClientCertificate is a task input property\n```\n\nA monitored API (`File`) on a raw task input — exactly what the analyzer exists to catch — invisible purely because of where it lived.\n\n**Resolution:** when you annotate a task, audit its full base chain up to `Task`/`ToolTask`. If the base holds shared input properties or path handling, have the **base** implement `IMultiThreadableTask` and do the resolution there, so every derived task inherits the fix:\n\n```csharp\npublic abstract class AkaMSLinksBase : Task, IMultiThreadableTask\n{\n    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;\n    // ... resolve ClientCertificate here, once, for all derived tasks\n}\n```\n\nNote that implementing `IMultiThreadableTask` on the base is safe and does not change routing — routing keys off the attribute, which stays on the concrete classes.\n\n### Engine-Owned Shared State: `RegisterTaskObject`\n\nThe guidance about `static` fields has a less obvious sibling: `IBuildEngine4.GetRegisteredTaskObject` / `RegisterTaskObject`. Under MT, in-process thread nodes share this engine-owned state. Tasks in different worker or TaskHost processes do not.\n\nUsing these APIs is a **review signal, not a warning**. Check object thread safety, key collisions, and whether correctness depends on two task invocations sharing a cache.\n\nThe get/register sequence is not atomic. Two tasks can both miss and create an object. Only one equal-key object is retained; `RegisterTaskObject` returns no result, and MSBuild does not dispose an unretained object. Whether that matters depends on what is cached:\n\n| Cached computation | Review result |\n|---|---|\n| Pure and deterministic for the key | Safe if the losing object needs no cleanup, or the task cleans it up |\n| Deduplication is the point, such as logging once per build | Unsafe because both tasks can perform the side effect |\n| A failure | Check that the key includes every input that affects the result; otherwise do not cache it |\n\nReal examples from dotnet/arcade: `LocateDotNet` was safe because its computation was pure. `SingleError` was not safe because its purpose was to emit exactly one error per build, so its annotation was removed.\n\n### Tasks Instantiated Directly by Other Tasks\n\n`[MSBuildMultiThreadableTask]` is only honored when the TaskFactory creates the task (i.e., a target invokes it as `<MyTask … />`). A task created via `new MyTask()` inside another task's body bypasses the factory and **never gets `TaskEnvironment` injected** — it silently defaults to `TaskEnvironment.Fallback` (= process CWD).\n\nMigrating a nested task in isolation is therefore meaningless. Either migrate the parent and have it explicitly propagate its `TaskEnvironment` to the nested instance before calling `Execute()`, or restructure so the nested task is a regular TaskFactory-created task.\n\n**Detect:** grep for `new …Task()` followed by `.Execute()` — every direct instantiation is a hole.\n\n### ToolTask Override Hot Spots\n\n`ToolTask` subclasses inherit virtual methods that run *before or after* `Execute()` and frequently touch the file system. Audit every override:\n\n| Override | Hazard | Migration |\n|---|---|---|\n| `GenerateFullPathToTool()` | Builds `ProcessStartInfo.FileName` — relative tool path → wrong tool launched | Absolutize the tool path before returning |\n| `SkipTaskExecution()` | Up-to-date check on input/output timestamps using relative paths | Absolutize both sides of the comparison |\n| `ValidateParameters()` | `File.Exists` on input parameters | Absolutize before probing |\n| `GenerateResponseFileCommands()` / `GenerateCommandLineCommands()` | Tempting to absolutize args | **Don't** — the child's `WorkingDirectory` is the project dir, so relative args resolve correctly; absolutizing inflates user-visible output and can leak into tool diagnostics or generated artifacts |\n| `GetWorkingDirectory()` | Default `null` → child inherits host CWD | Leave alone if you use `TaskEnvironment.GetProcessStartInfo()`; it already sets `WorkingDirectory` |\n\n**Key contract:** `ProcessStartInfo.FileName` is resolved by the OS **before** `WorkingDirectory` takes effect, so the executable path must be absolute. Tool arguments are interpreted by the child process in its working directory, so they should stay relative.\n\n### Engine-Owned Environment Variables Are Immutable in MT Mode\n\nEnv vars consumed by the engine *before* tasks run (e.g., `MSBUILD*`-prefixed flags and the framework/SDK discovery variables) are snapshotted into engine caches at startup. Tasks that mutate them later have no observable effect on the engine — but the mutation appears to succeed locally, masking bugs.\n\nIf a migrated task previously mutated such a variable to influence a downstream tool, re-architect to pass the value via `ProcessStartInfo.EnvironmentVariables` on the child process instead of mutating the parent's environment.\n\n---\n\n## Test Patterns for MT Migrations\n\nA migration test must **fail when the migration is undone**. Tests that pass identically against pre- and post-migration code are theater. The two patterns below are the ones that reliably exercise MT-specific behavior.\n\n### Pattern A: Decoy-CWD Test (for tasks that touch the file system)\n\nSet the process working directory to a **decoy** dir with no relevant files; set `TaskEnvironment.ProjectDirectory` to a different dir with the inputs/expected outputs. The task must read/write against the project directory, not the decoy.\n\n```csharp\nusing TestEnvironment env = TestEnvironment.Create();\nTransientTestFolder projectDir = env.CreateFolder();\nTransientTestFolder decoyCwd = env.CreateFolder();\nFile.WriteAllText(Path.Combine(projectDir.Path, \"input.txt\"), \"expected\");\n\nenv.SetCurrentDirectory(decoyCwd.Path); // auto-restored when env is disposed\n\nvar task = new MyTask\n{\n    TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir.Path),\n    Input = \"input.txt\", // relative\n};\ntask.Execute().ShouldBeTrue();\n```\n\n**CRITICAL:** `Directory.SetCurrentDirectory` is **process-global**. xUnit parallelizes tests within a class by default — a CWD-mutating test will flake unless pinned to a non-parallel collection (`[Collection]` + `[CollectionDefinition(DisableParallelization = true)]`). The `IDisposable` restore protects sequential safety but not concurrent siblings.\n\n### Pattern B: Cross-Instance Independence (for tasks doing relative-path resolution)\n\nTwo task instances, two `TaskEnvironment.ProjectDirectory` values, same relative input. Assert each task's output is rooted to its own project directory — proving resolution is per-instance, not shared.\n\n### When NOT to Write a \"Migration Test\"\n\nAttribute-only migrations (just `[MSBuildMultiThreadableTask]`, no `IMultiThreadableTask`) on tasks whose `Execute()` body contains no file/env/process/static-state interactions have nothing to test — a decoy-CWD test would pass whether the attribute is present or not. **Don't write the test.** Document the call-chain audit conclusion in the PR description instead.\n\n### Test Hygiene\n\n- Use `TestEnvironment` / `TransientTestFolder` for temp dirs; never `Path.GetTempPath() + Guid.NewGuid()` with manual `try/finally`.\n- Hard-code expected assertion values; don't recompute them from `Path.Combine(...)` in the assertion — when the test fails you want to know what was actually wrong.\n\n---\n\n## Verification: A Clean Analyzer Run Is Not a Migration\n\n`Microsoft.Build.TaskAuthoring.Analyzer` is the right first line of defense, and it should be enabled. It is not sufficient on its own, and it is worth being concrete about why so you know what work it leaves you.\n\n**The data.** In a migration of ~150 tasks across dotnet/arcade, dotnet/source-build-assets and the dotnet/dotnet VMR, the analyzer was enabled in all three repos and every build was **0 warnings, 0 errors**. A manual audit afterwards found **9 real defects**:\n\n| Why the analyzer missed it | Count | Example |\n|---|---|---|\n| The code was outside the analysis scope | 2 | unsafe call in an unannotated base class |\n| The API is not on the monitored list | 3 | `AssemblyName.GetAssemblyName` |\n| The failure class is not modeled at all | 4 | task-object race, memoized failure, nested task construction, path crossing a DI boundary |\n\nThe last row is the important one: those are dataflow and lifetime problems, not banned-API problems. No amount of allowlist tuning reaches them, which is why the [Red-Team Audit Protocol](#red-team-audit-protocol) below is not optional.\n\n**Why \"it worked when I tested it\" is weak evidence.** An unresolved relative path in MT mode does not reliably throw. It resolves against whatever the shared node's current directory happens to be, which depends on which project scheduled first. So the same defect passes locally single-threaded every time, passes in CI most of the time, and fails occasionally on one machine under load. And per [Sin 8](#sin-8-swallowed-exceptions-hiding-an-unresolved-path), some never throw at all.\n\n**Verification checklist.** The [Sign-Off Checklist](#sign-off-checklist) at the end of this document is the actionable list. The items most likely to be skipped — because nothing warns you about them — are:\n\n1. The full base chain, not just the concrete class.\n2. Every `catch`, classified: does it mean \"fail\", or a semantic answer? (Sin 8)\n3. Every task input traced to *every* use site — the common defect is resolved at 3 of 4 sites, not 0 of 4.\n4. Caches and memoization: `static` fields, `RegisterTaskObject`, and anything storing a failure.\n5. A Pattern A or Pattern B test that fails when the migration is reverted — asserting on the *result*, not merely that `Execute()` returned true.\n\nIf your host supports agents, the `mt-migration-reviewer` agent automates most of this.\n\n## Migrating Is Not Always the Right Answer\n\nAdding the attribute is a claim that the task is safe to run concurrently in a shared process. When that claim cannot be made honestly, **leaving the task unannotated is a correct, supported outcome** — it simply keeps routing to the TaskHost sidecar, which is exactly the behavior it has today. A slower task is better than a wrong one.\n\nDe-annotate (or never annotate) when:\n\n- The task's semantics depend on being a singleton within the build — `SingleError` above is the canonical case.\n- The task deliberately mutates process-global state for a downstream consumer and cannot be re-architected to pass it via `ProcessStartInfo`.\n- The correctness argument depends on a race being benign and you cannot convince yourself it is.\n\nWhen you do this, leave a comment saying *why*, so the next person does not \"fix\" the missing attribute:\n\n```csharp\n// Deliberately not [MSBuildMultiThreadableTask]: this task must report exactly\n// one error per build, and the Get/RegisterTaskObject pair used to enforce that\n// is not atomic across concurrent instances in a shared node.\n```\n\n---\n\n## Red-Team Audit Protocol\n\n### Phase 1: Trace Every Changed Line\n\nFor each modified line: What was the exact runtime value before? After? Where does it flow (outputs, logs, file paths, dictionary keys)? Does each destination produce identical observable behavior?\n\n### Phase 2: Null/Empty/Edge Input Analysis\n\n| Input | `GetAbsolutePath` | Old behavior | Match? |\n|---|---|---|---|\n| `null` | `ArgumentException` | Varies | ❓ |\n| `\"\"` | `ArgumentException` | Varies | ❓ |\n| `\"C:\\\"` (root) | Valid | Valid | ✅ usually |\n| `\".\"` | `\"C:\\repo\\.\"` (not canonical) | `\"C:\\repo\"` if GetFullPath | ❌ maybe |\n| `\"foo\\..\\bar\"` | `\"C:\\repo\\foo\\..\\bar\"` | `\"C:\\repo\\bar\"` if GetFullPath | ❌ maybe |\n| Already absolute | Pass-through | Pass-through | ✅ |\n\n`Path.GetDirectoryName` → `Path.Combine` chains:\n| Input | `GetDirectoryName` returns | `Path.Combine(result, x)` |\n|---|---|---|\n| `\"C:\\\"` | `null` | Throws `ArgumentNullException` |\n| `\"\"` | `\"\"` (.NET Fx) / `null` (.NET Core+) | Works / Throws ⚠️ |\n| `\"file.resx\"` (no dir) | `\"\"` | Works |\n\nVerify behavior on **both** .NET Framework and .NET TFMs.\n\n### Phase 3: Downstream Impact\n\n1. **Output properties**: What consumes this task's `[Output]`? Does it compare, display, or use as a path?\n2. **Written files**: Does file content change? (e.g., XML with embedded paths)\n3. **Helper methods**: Do `LockCheck`, `ManifestWriter`, etc. internally resolve relative paths?\n4. **Error codes**: MSBuild error codes (MSBxxxx) must be identical.\n\n### Phase 4: Concurrency\n\n1. Two tasks with different `ProjectDirectory` values don't interfere\n2. No writes to static fields (shared across threads)\n3. Every registered task object is reviewed for thread safety, key collisions, registration races, and process-local visibility\n4. No cached *failures* — a failure computed under one task's environment must not be served to another\n5. All file operations use absolutized paths\n\n## Compatibility Test Matrix\n\n```\n[Task] × [Input Type] × [Assertion]\n\nInputs: relative path, absolute path, null, empty, \"..\" segments, root \"C:\\\",\n        forward slashes, trailing separator, UNC path, 260+ char path\n\nAssertions: Execute() return value, [Output] exact string, error message content,\n            exception type, file location, file content\n```\n\n## Sign-Off Checklist\n\n- [ ] `[MSBuildMultiThreadableTask]` on every concrete class (not just base — `Inherited=false`)\n- [ ] **Base chain audited** up to `Task`/`ToolTask` — unannotated bases still run multithreaded and are *not* analyzed\n- [ ] `IMultiThreadableTask` on classes that use `TaskEnvironment` APIs, with default initializer `= TaskEnvironment.Fallback`\n- [ ] Every `[Output]` property: exact string value matches pre-migration (Sin 1)\n- [ ] Every `Log.LogError`/`LogWarning`: path in message matches pre-migration (use `OriginalValue`) (Sin 2)\n- [ ] Every `Log.LogError(ex.Message …)` / `ex.FileName`: exception path sanitized to original input (Sin 2)\n- [ ] Every `GetAbsolutePath` call: null/empty exception behavior matches old code path (Sin 3, 6)\n- [ ] Every dictionary/set with path keys: canonicalization preserved (`GetCanonicalForm()`) (Sin 5)\n- [ ] Every try-catch: absolutized value available in catch block where needed (Sin 4)\n- [ ] Every `??` or `?.` added: verified it doesn't swallow a previously-thrown exception (Sin 3)\n- [ ] No `Path.IsPathRooted` short-circuits around `GetAbsolutePath` — call unconditionally (Sin 7)\n- [ ] Every `catch` classified — any handler meaning a *semantic answer* rather than \"fail\" has all path calls in its `try` absolutized, and is narrowed to the types representing that answer (Sin 8)\n- [ ] **Analyzer-invisible path consumers** absolutized: `AssemblyName.GetAssemblyName`, `XDocument`/`XmlDocument.Load(string)`, `XmlReader.Create(string)`, `ZipFile.*`, `X509CertificateLoader`, `Assembly.LoadFrom` — none of these produce a diagnostic\n- [ ] Every task input resolved at **every** use site, not most of them\n- [ ] Task inputs crossing an interface / delegate / DI boundary (`IFileSystem`, `ICommandFactory`) resolved at the task boundary\n- [ ] No `AbsolutePath` leaks into user-visible strings unintentionally\n- [ ] No behavior conditioned on \"MT mode on/off\" — `TaskEnvironment.Fallback` handles single-process case\n- [ ] **Call chain traced end-to-end** for every helper invoked from `Execute()`:\n    - No `Environment.CurrentDirectory` / `Directory.GetCurrentDirectory()` / `Path.GetFullPath(x)` without a base anywhere in the transitive call graph\n    - No direct `Environment.Get/SetEnvironmentVariable` (route through `TaskEnvironment`)\n    - No `static` mutable fields seeded from process state; replace with `ConcurrentDictionary` keyed on inputs\n    - Every `RegisterTaskObject` use reviewed for object thread safety, key collisions, registration races, and process-local visibility\n    - No cached *failures* whose key omits inputs that affect the result\n    - No `Console.*`, `Environment.Exit`, `Process.Kill`, `FailFast`\n- [ ] ToolTask overrides audited (`GenerateFullPathToTool`, `SkipTaskExecution`, `ValidateParameters`); `ProcessStartInfo.FileName` is absolute, tool *arguments* stay relative\n- [ ] No nested tasks created via `new …Task()` without explicit `TaskEnvironment` propagation\n- [ ] No mutations of engine-owned env vars (e.g. `MSBUILD*` and the framework/SDK discovery vars) in MT mode\n- [ ] Tests for custom tasks set `TaskEnvironment = TaskEnvironmentHelper.CreateForTest()` (built-in tasks have a default)\n- [ ] Migration test follows Pattern A (decoy-CWD) **or** Pattern B (cross-instance independence), or PR explains why no test is meaningful\n- [ ] CWD-mutating tests pinned to a non-parallel xUnit collection\n- [ ] Cross-framework: tested on .NET Framework and .NET TFMs.\n- [ ] If the task was **deliberately left unannotated**, a comment records why (see [Migrating Is Not Always the Right Answer](#migrating-is-not-always-the-right-answer))\n","src/StringTools/README.md":"# Microsoft.NET.StringTools\n\nThis package contains the Microsoft.NET.StringTools assembly which implements common string-related functionality such as weak interning.\n\nAt this time, this is primarily an internal implementation detail of MSBuild and Visual Studio and we do not expect other consumers of the package. If you think you might like to use it, please start a discussion at https://github.com/dotnet/msbuild/discussions to let us know your use cases.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Agent Instructions\n\nInstructions for GitHub Copilot and other AI coding agents working with the MSBuild repository.\n\n## Repository Overview\n\n**MSBuild** is the Microsoft Build Engine - performance-critical infrastructure for .NET and Visual Studio builds. This repository contains the code for the MSBuild build engine, including its public C# API, its internal implementation of the MSBuild programming language, and core targets and tasks used for builds.\n\n### Key Components\n- **Microsoft.Build**: Core MSBuild engine and public API\n- **Microsoft.Build.Framework**: Framework interfaces and base types\n- **Microsoft.Build.Tasks**: Built-in MSBuild tasks\n- **Microsoft.Build.Utilities**: Utility classes for task authors\n- **MSBuild CLI**: Command-line tool for invoking builds\n\n### Technology Stack\n- .NET 10.0 and .NET Framework 4.7.2\n- C# 14 features (especially collection expressions)\n- xUnit with Shouldly for testing\n- Multi-platform support (Windows, Linux, macOS)\n\n## General\n\n* Performance is the top priority - minimize allocations, avoid LINQ in hot paths, use efficient algorithms.\n* Always use the latest C# features, currently C# 14, especially collection expressions (`[]` over `new Type[]`).\n* Match the style of surrounding code when making edits, but modernize aggressively for substantial changes.\n\n## Code Review Instructions\n\nOfficial builds treat all warnings as errors, so do not introduce new warnings.\n\n### Performance Considerations\n\nWhen reviewing pull requests:\n\n* **Flag any unnecessary allocations** in hot paths\n* **Flag LINQ usage** in performance-critical code paths\n* **Check for proper use of `Span<T>`** and `ReadOnlySpan<T>` for string parsing\n* **Ensure immutable collections** use the correct type (`ImmutableArray<T>` and `FrozenDictionary<TKey, TValue>` for read-heavy, `ImmutableList<T>` for incremental building)\n\n### NuGet Feed Configuration\n\nWhen reviewing pull requests:\n\n* **Flag any changes to NuGet.config** that add external package sources without justification\n* Package sources should use approved internal feeds when possible\n\n## Formatting\n\n* Apply code-formatting style defined in `.editorconfig`.\n* Prefer file-scoped namespace declarations and single-line using directives.\n* Insert a newline before the opening curly brace of any code block.\n* Use pattern matching and switch expressions wherever possible.\n* Use `nameof` instead of string literals when referring to member names.\n\n### Nullable Reference Types\n\n* **New files**: Always use nullable reference types (do NOT add `#nullable disable`)\n* **Existing files with `#nullable disable`**: Match the existing style; don't add nullable annotations (`?`) to types\n* **Existing files with nullable enabled**: Use proper nullable annotations\n* Always use `is null` or `is not null` instead of `== null` or `!= null`\n\n## Performance Best Practices\n\n### Range Pattern Matching\n\n```csharp\n// GOOD: Clear and efficient\nreturn errorNumber switch\n{\n    >= 3001 and <= 3999 => Category.Tasks,\n    >= 4001 and <= 4099 => Category.General,\n    >= 4100 and <= 4199 => Category.Evaluation,\n    _ => Category.Other\n};\n```\n\n### String Handling\n\n* Use `MSBuildNameIgnoreCaseComparer` for case-insensitive comparisons of MSBuild names; use `StringComparer.OrdinalIgnoreCase` only for non-MSBuild string comparisons\n* Use `char.ToUpperInvariant()` for single-character comparisons\n* Use `ReadOnlySpan<char>` and `Slice()` to avoid string allocations\n* Use `int.TryParse(span, out var result)` on .NET Core+ for allocation-free parsing\n\n### Inlining Hot Paths\n\n```csharp\n[MethodImpl(MethodImplOptions.AggressiveInlining)]\nprivate static bool IsCompilerPrefix(string value) => ...\n```\n\n### Immutable Collections\n\n**Build once, read many times** (most common in MSBuild):\n```csharp\nImmutableArray<string> items = source.Select(x => x.Name).ToImmutableArray();\nFrozenDictionary<string, int> lookup = pairs.ToFrozenDictionary(x => x.Key, x => x.Value);\n```\n\n**Build incrementally over time**:\n```csharp\nImmutableList<string> items = ...;  // Use when adding items one by one\nImmutableDictionary<string, int> lookup = ...;\n```\n\n## Building\n\nMost development can use `dotnet build`. For the most complete possible build, use `MSBuild.exe` from Visual Studio 2026. See the [Windows build prerequisites](README.md#building-msbuild-with-visual-studio-2026-on-windows).\n\nNEVER pipe MSBuild output to a file. If you want a list of errors, add `-flp:\"v=q;LogFile=ErrorsAndWarnings.log\"` to the arguments.\n\nWhen considering a subset of build errors, always look at the BEGINNING of the set, not the end.\n\n## Individual projects\n\nBuild individual projects with `dotnet msbuild {path/to/project.csproj} -v:q`.\n\n### Whole-repo Build Commands\n\n| Platform | Command | Timeout |\n|----------|---------|---------|\n| Windows | `.\\build.cmd -v quiet` | 300+ seconds (~2-3 minutes) |\n| macOS/Linux | `./build.sh -v quiet` | 300+ seconds (~2-3 minutes) |\n\n### Bootstrap Environment Setup\n\nAfter building the whole repo, to use the just-built MSBuild to build things use the bootstrap environment (described in [`skills/use-bootstrap-msbuild](.github/skills/use-bootstrap-msbuild/SKILL.md)).\n\n### Build Troubleshooting\n\n* If build fails with \"Could not resolve SDK\", run the bootstrap environment script\n* Verify `dotnet --version` shows the preview/internal version\n* Use repository sample projects for testing, not external projects\n* Build artifacts go to `./artifacts/` directory\n\n### Running Tests\n\n**Windows:**\n```cmd\n# Full test suite (~9 minutes) - NEVER CANCEL\n.\\build.cmd -test\n\n# Individual test project (recommended):\ndotnet test src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj\n```\n\n**macOS/Linux:**\n```bash\n# Full test suite (~9 minutes) - NEVER CANCEL\n./build.sh --test\n\n# Individual test project (recommended):\ndotnet test src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj\n```\n\nYou can run a single unit test with `-- --filter-method \"*{methodname}*\"`, for example `dotnet test src/Framework.UnitTests/ -- --filter-method \"*ExerciseBuildEventContext*\"`.\n\n### Test Verification\n\n* **Individual Test Project**: ~10-60 seconds per project\n* **Full Test Suite**: ~9 minutes\n\n## Project Layout and Architecture\n\n### Directory Structure\n\n```\nsrc/\n├── Build/                    # Core MSBuild engine (Microsoft.Build)\n├── Build.UnitTests/          # Unit tests for core engine\n├── MSBuild/                  # MSBuild command-line tool\n├── Framework/                # MSBuild Framework (Microsoft.Build.Framework)\n├── Framework.UnitTests/      # Unit tests for framework\n├── Tasks/                    # Built-in MSBuild tasks (Microsoft.Build.Tasks)\n├── Tasks.UnitTests/          # Unit tests for tasks\n├── Utilities/                # MSBuild utilities (Microsoft.Build.Utilities)\n├── Utilities.UnitTests/      # Unit tests for utilities\n├── Shared/                   # Shared code across assemblies\n└── Samples/                  # Sample projects and extensions\n\nartifacts/\n├── bin/                      # Built binaries\n│   └── bootstrap/\n│       └── core/\n│           └── MSBuild.dll   # Built MSBuild executable\n├── sdk-build-env.sh          # Bootstrap script (Linux/macOS)\n├── msbuild-build-env.bat     # Bootstrap script (Windows)\n└── packages/                 # Built NuGet packages\n\ndocumentation/\n├── wiki/                     # Developer documentation\n├── specs/                    # Technical specifications\n└── *.md                      # Various documentation files\n```\n\n### Key Configuration Files\n\n* **`global.json`**: Pins .NET SDK version - never modify without explicit request\n* **`NuGet.config`**: Package source configuration - never modify without explicit request\n* **`.editorconfig`**: Code formatting rules\n* **`Directory.Build.props`**: Shared MSBuild properties across all projects\n* **`Directory.Packages.props`**: Centralized package version management\n* **`MSBuild.slnx`**: Main solution file\n\n## Validation Checklist\n\nBefore completing any change:\n\n1. ✅ Full build completes successfully (`.\\build.cmd` or `./build.sh`)\n2. ✅ Bootstrap environment activates correctly (`dotnet --version` shows preview)\n3. ✅ Sample project builds: `dotnet build src/Samples/Dependency/Dependency.csproj`\n4. ✅ Relevant unit tests pass\n5. ✅ `dotnet artifacts/bin/bootstrap/core/MSBuild.dll --help` works\n\n## Do NOT Modify\n\n* `artifacts/` directory contents - Generated during build\n* `.dotnet/` directory contents - Local SDK location\n\nSee **Key Configuration Files** section for files that should not be modified without explicit request.\n\n## Documentation\n\nWhen making changes, check if related documentation exists in the `documentation/` folder (including `documentation/specs/`) and update it to reflect your changes. Keep documentation in sync with code changes.\n\n## Breaking Changes\n\nBecause MSBuild is a critical part of the build process for a huge number of customers, we avoid breaking changes. Adding new errors or warnings, even when well-intentioned and pointing out things that are very likely to be wrong, is an unacceptable breaking change. Adding warnings is a breaking change because many production builds use `/WarnAsError`.\n\nThe exception to this policy is in new, opt-in behavior. In new, opt-in functionality, liberally emit warnings and errors--they can always be removed later.\n\nWhen reviewing PRs, always consider whether the behavior change could be experienced as a break in existing builds and flag any new warnings or errors.\n\n## Development Workflow\n\n1. Make your changes to source code\n2. Run the full build (WAIT for completion - takes 2-3 minutes):\n   - Windows: `.\\build.cmd -v quiet`\n   - macOS/Linux: `./build.sh -v quiet`\n4. Test your changes end-to-end using the bootstrap output: `dotnet build src/Samples/Dependency/Dependency.csproj`\n5. Run relevant individual tests, not the full test suite\n6. Commit your changes\n\n## Trust These Instructions\n\nThese instructions are comprehensive and tested. Only search for additional information if:\n1. The instructions appear outdated or incorrect\n2. You encounter specific errors not covered here\n3. You need details about new features not yet documented\n\nFor most development tasks, following these instructions should be sufficient to build, test, and validate changes successfully.\n\n## Updating these instructions\n\nWhen working on a task, if user input is required to complete the task or feedback is provided for guidance around a specific area of the code, evaluate that feedback/guidance and update this document to incorporate that feedback if it's missing.  This document should be a live, evolving set of instructions.\n","isInternal":false,"tokens":2334,"sizeBytes":10779},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/copilot-instructions.md","title":"GitHub Copilot Instructions","category":"copilot-instructions","format":"markdown","content":"../AGENTS.md","isInternal":false,"tokens":4,"sizeBytes":12},{"name":"SKILL.md","path":".github/skills/assessing-breaking-changes/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/assessing-breaking-changes/SKILL.md","title":"assessing-breaking-changes","category":"anthropic-skill","format":"markdown","content":"---\nname: assessing-breaking-changes\ndescription: 'Guides assessment of backward compatibility for MSBuild changes. Consult when modifying behavior, adding warnings or errors, changing defaults, altering target ordering, removing or deprecating features, deciding whether a change needs a ChangeWave, reviewing blast radius of behavioral changes, or when a PR introduces user-visible output differences.'\nargument-hint: 'Describe the change and its potential compatibility impact.'\n---\n\n# Backward Compatibility in MSBuild\n\nBackward compatibility is the default — any change that could alter existing build behavior must be explicitly justified.\n\nThis skill covers **how to evaluate compatibility risk**. For the mechanics of ChangeWave implementation, see the [changewaves skill](../changewaves/SKILL.md).\n\n## Core Philosophy\n\n1. **Existing builds must not break.** If a project built successfully yesterday, it must build successfully today with identical semantics.\n2. **New warnings are breaking changes.** Builds that use `-WarnAsError` or `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` will fail if you introduce a new warning. Always gate new warnings behind a ChangeWave or emit them as `Message` importance instead.\n3. **Output changes are behavioral changes.** Even \"improvements\" to output formatting, file paths, or diagnostic text can break downstream consumers that parse MSBuild output.\n4. **Removal is nearly impossible.** Never remove CLI switches, public APIs, or property names. Deprecate with warnings first, then gate removal behind a ChangeWave after multiple release cycles.\n\n## Blast Radius Checklist\n\nBefore merging any behavioral change, evaluate:\n\n| Question | If Yes |\n|----------|--------|\n| Does this change what gets built or how? | ChangeWave required |\n| Does this add a new warning? | ChangeWave required (WarnAsError impact) |\n| Does this change a property default? | ChangeWave required (existing .csproj files depend on defaults) |\n| Does this alter target execution order? | Test with real-world solutions; likely ChangeWave |\n| Does this change console or binlog output format? | Consider downstream tool impact |\n| Does this affect only internal code paths with no user-visible effect? | No ChangeWave needed |\n| Is this a pure bug fix restoring documented behavior? | Usually no ChangeWave; use judgment on blast radius |\n\n## When ChangeWave Is NOT Needed\n\n- Internal refactoring with no observable behavior change\n- Performance improvements that don't change semantics\n- New opt-in features gated by a new property or switch\n- Bug fixes that restore clearly-intended behavior with limited blast radius\n\nFor detailed ChangeWave mechanics, see [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md) and [ChangeWaves.md](../../../documentation/wiki/ChangeWaves.md).\n\n## The Warnings-as-Errors Rule\n\nThis is the most commonly missed compatibility concern:\n\n```xml\n<!-- Many enterprise builds set this globally -->\n<TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n```\n\nAny new `MSBxxxx` warning you add will **break these builds**. Options:\n1. **Gate behind ChangeWave** — preferred for genuinely important warnings\n2. **Use `MessageImportance.Low` or `Normal`** — for informational diagnostics\n3. **Add as an error from the start** — if the condition is always wrong\n\n## Deprecation Protocol\n\n1. Add a deprecation warning (behind ChangeWave if broad impact)\n2. Document the deprecation in release notes\n3. Maintain the old behavior for at least two major .NET versions\n4. Only remove after the ChangeWave has rotated out\n\n## Compatibility Test Matrix\n\nWhen testing backward compatibility, verify:\n\n- **Multi-targeting projects** — `<TargetFrameworks>net472;net8.0</TargetFrameworks>`\n- **Solution builds with mixed project types** — C#, F#, VB, C++/CLI\n- **Incremental builds** — second build should be a no-op\n- **Design-time builds** — Visual Studio calls different target contracts\n- **Cross-platform** — path separator differences, case sensitivity on Linux\n- **WarnAsError builds** — explicitly test with `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>`\n\n## Decision Framework\n\n```\nIs the change user-visible?\n├── No → Ship it (no ChangeWave needed)\n└── Yes\n    ├── Is it a new opt-in feature? → Ship it (no ChangeWave needed)\n    └── Does it change existing behavior?\n        ├── Bug fix with low blast radius? → Ship it, add regression test\n        └── Behavioral change or new warning?\n            └── Gate behind ChangeWave, test opt-out path\n```\n\nDocument the compatibility decision in your PR description so reviewers can validate it.\n","frontmatter":{"name":"assessing-breaking-changes","description":"Guides assessment of backward compatibility for MSBuild changes. Consult when modifying behavior, adding warnings or errors, changing defaults, altering target ordering, removing or deprecating features, deciding whether a change needs a ChangeWave, reviewing blast radius of behavioral changes, or when a PR introduces user-visible output differences.","argument-hint":"Describe the change and its potential compatibility impact."},"isInternal":false,"tokens":992,"sizeBytes":4681},{"name":"SKILL.md","path":".github/skills/authoring-errors-and-warnings/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/authoring-errors-and-warnings/SKILL.md","title":"authoring-errors-and-warnings","category":"anthropic-skill","format":"markdown","content":"---\nname: authoring-errors-and-warnings\ndescription: 'Guides authoring of MSBuild errors, warnings, and diagnostic messages. Consult when adding new MSBxxxx codes, writing or modifying user-facing diagnostic text, deciding between error/warning/message severity, working with Strings.resx resource files, formatting paths in error output, or evaluating whether a new warning could break WarnAsError builds.'\nargument-hint: 'Describe the error/warning being added or modified.'\n---\n\n# Error and Warning Authoring in MSBuild\n\nError messages are MSBuild's primary user interface. They must help developers fix problems without reading source code.\n\nFor the mechanics of error code assignment, see [assigning-msb-error-code.md](../../../documentation/assigning-msb-error-code.md).\n\n## Error Message Quality Rules\n\nEvery error message must answer three questions:\n\n1. **What happened?** — State the problem clearly\n2. **Why?** — Provide context (file, property, expected value)\n3. **What should the user do?** — Give actionable guidance\n\n```xml\n<!-- BAD: What is \"it\"? What should I do? -->\n<value>MSB4999: Invalid configuration.</value>\n\n<!-- GOOD: States the problem, context, and fix -->\n<value>MSB4999: The project \"{0}\" specifies TargetFramework \"{1}\" which is not installed. Install the SDK for \"{1}\" or update the TargetFramework in the project file.</value>\n```\n\n## Severity Decision Framework\n\n```\nIs the condition always wrong (invalid input, impossible state)?\n├── Yes → ERROR (build should fail)\n└── No\n    ├── Could this cause subtle build correctness issues?\n    │   ├── Yes, likely → WARNING (but see WarnAsError impact below)\n    │   └── Yes, maybe → MESSAGE at Normal importance\n    └── Is this purely informational?\n        └── Yes → MESSAGE at Low importance\n```\n\n### The WarnAsError Constraint\n\n**New warnings are breaking changes** for builds using:\n- `-WarnAsError` (CLI)\n- `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` (project)\n- `<WarningsAsErrors>MSBxxxx</WarningsAsErrors>` (specific codes)\n\nBefore adding a new warning, consider:\n1. **Is the warning important enough to justify breaking WarnAsError builds?** If yes, gate behind a ChangeWave (see [changewaves skill](../changewaves/SKILL.md)).\n2. **Could this be a Message instead?** Messages never break builds but still appear in binary logs.\n3. **Should this be an error from the start?** If the condition is always wrong, skip the warning stage.\n\n## MSB Error Code Assignment\n\n### Code Ranges\n\n| Range | Area |\n|-------|------|\n| `MSB1xxx` | Command-line handling |\n| `MSB3xxx` | Tasks (`Microsoft.Build.Tasks.Core.dll`) |\n| `MSB4xxx` | Engine (`Microsoft.Build.dll`) |\n| `MSB5xxx` | Shared code across assemblies |\n| `MSB6xxx` | Utilities (`Microsoft.Build.Utilities`) |\n\n### Assignment Process\n\n1. Open the `Strings.resx` file for the appropriate assembly\n2. Find the \"Next message code\" comment at the bottom of the file\n3. Search the repo to confirm the code is not already in use\n4. Use the code; update the comment to the next available number\n\nSee [assigning-msb-error-code.md](../../../documentation/assigning-msb-error-code.md) for detailed steps.\n\n## Resource String Format\n\n```xml\n<data name=\"FeatureArea.DescriptiveName\">\n  <value>MSBxxxx: Clear description with {0} placeholders for runtime values.</value>\n  <comment>{StrBegin=\"MSBxxxx: \"}{0} is the project file path. {1} is the property name.</comment>\n</data>\n```\n\n### Rules\n\n- Resource name uses `FeatureArea.DescriptiveName` convention\n- Value starts with `MSBxxxx: ` (code, colon, space)\n- Comment documents the `{StrBegin}` marker and explains each `{N}` placeholder\n- Comments guide translators — mention untranslatable tokens\n\n## Consuming Error Resources in Code\n\nUse the standard formatting method that extracts and applies the error code. Engine-level project\nerrors use `Microsoft.Build.Internal.ProjectErrorUtilities`:\n\n```csharp\n// For errors\nLog.LogErrorWithCodeFromResources(\"Copy.Error\", sourceFile, destFile, ex.Message);\n\n// For warnings\nLog.LogWarningWithCodeFromResources(\"ResolveAssemblyReference.Conflict\", assemblyName);\n\n// For engine-level errors (not in tasks)\nProjectErrorUtilities.ThrowInvalidProject(\n    elementLocation,\n    \"InvalidProjectFile\",\n    arg1, arg2);\n```\n\n**Never** construct error messages by string concatenation — always use resource strings for localization support.\n\n## Localization Requirements\n\n- Error **codes** (`MSBxxxx`) are never localized\n- Error **text** is localized into all supported languages\n- After adding/modifying resource strings, run a full build to generate `.xlf` placeholder translations\n- Use `<comment>` elements to help translators understand context\n- See [Localization.md](../../../documentation/wiki/Localization.md) for the full localization process\n\n## Path Formatting in Error Messages\n\n- Show paths relative to the project directory when possible\n- Use the path exactly as the user specified it (don't normalize slashes)\n- Include file and line number via `IElementLocation` when available — this enables IDE click-to-navigate\n- For paths that could contain spaces, ensure they're quoted in the message\n\n## Checklist for New Errors/Warnings\n\n- [ ] Error code assigned from correct `Strings.resx` range\n- [ ] \"Next message code\" comment updated in the resx\n- [ ] Message answers: what happened, why, what to do\n- [ ] `{StrBegin}` comment and placeholder documentation added\n- [ ] Severity chosen (error vs warning vs message) with WarnAsError considered\n- [ ] If warning: ChangeWave gating evaluated\n- [ ] Full build run to generate `.xlf` files\n- [ ] Test verifies the error is produced with correct code and text\n","frontmatter":{"name":"authoring-errors-and-warnings","description":"Guides authoring of MSBuild errors, warnings, and diagnostic messages. Consult when adding new MSBxxxx codes, writing or modifying user-facing diagnostic text, deciding between error/warning/message severity, working with Strings.resx resource files, formatting paths in error output, or evaluating whether a new warning could break WarnAsError builds.","argument-hint":"Describe the error/warning being added or modified."},"isInternal":false,"tokens":1300,"sizeBytes":5711},{"name":"SKILL.md","path":".github/skills/changewaves/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/changewaves/SKILL.md","title":"changewaves","category":"anthropic-skill","format":"markdown","content":"---\nname: changewaves\ndescription: 'Manage MSBuild Change Waves: create new waves, condition features behind opt-out flags, write tests for wave-gated features, document change waves in ChangeWaves.md, and retire expired waves. Use when adding changes that need an opt-out or rotating out old change waves. Changes that introduce a user-visible behavior change should consider whether to use a changewave.'\nargument-hint: 'Add, query, or remove changewaves and changewave checks.'\n---\n\n# Managing MSBuild Change Waves\n\nA Change Wave is an opt-out flag that groups risky features together. Users disable features by setting the environment variable `MSBUILDDISABLEFEATURESFROMVERSION` to the wave version. This skill covers the **how** — the full lifecycle: creating a wave, conditioning code on it, testing, documenting, and retiring.\n\nFor the **when** — deciding whether a change is a breaking change and whether it needs a ChangeWave at all — see [assessing-breaking-changes](../assessing-breaking-changes/SKILL.md).\n\n\n## Decide Whether a Change Wave Is Appropriate\n\nUse a change wave when a change is valuable but has meaningful compatibility risk for existing builds.\n\nGood candidates:\n- User-visible behavior changes that may regress some build graphs.\n- Changes in parsing/evaluation/execution semantics where real-world usage is broad or hard to predict.\n- Changes that may require customer adaptation time and benefit from a temporary opt-out.\n\nUsually avoid a change wave for:\n- Pure bug fixes that restore intended existing behavior with low compatibility risk.\n- Internal refactoring/perf work with no externally observable behavior change.\n- New functionality that is already explicitly opt-in (for example, gated by a new property, switch, or API surface).\n\nIf uncertain, default to safety:\n1. Assess blast radius and rollback difficulty.\n2. Check whether the change could be experienced as a breaking change in production builds.\n3. Use a wave if an opt-out is likely needed while customers adapt.\n\nDocument the decision in PR notes so reviewers can validate the call.\n\nIf a changewave is appropriate, consult [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md) (developer-facing) and [ChangeWaves.md](../../../documentation/wiki/ChangeWaves.md) (public-facing) for details on how to follow the below steps.\n\n## Step 1: Determine the Correct Wave Version\n\nLook up the current MSBuild version. If a wave already exists for that version in `src/Framework/ChangeWaves.cs`, use it. Otherwise, create a new one.\n\n## Step 2: Condition Your Feature on the Wave\n\nC# and MSBuild examples are in [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md). Prefer to put checks inline rather than abstracting out a method to host the check.\n\n## Step 3: Write Tests\n\nWrite normal tests for the new behavior. Then add at least one test that verifies the old behavior is **preserved** when the wave is opted out.\n\n## Step 4: Document the Feature\n\nAdd an entry to the **Current Rotation of Change Waves** section in [ChangeWaves.md](../../../documentation/wiki/ChangeWaves.md).\n\n- If a heading for this wave already exists, add a bullet under it.\n- If not, add a new `### {Major}.{Minor}` heading at the top of the current rotation list.\n\nEach entry is a bullet with a link to the PR:\n\n```markdown\n### 18.6\n- [Short description of the change.](https://github.com/dotnet/msbuild/pull/NNNNN)\n```\n\n## Only when explicitly requested: Retire an Expired Wave\n\nWaves rotate out when a new major .NET version will be released. When retiring a wave, follow the detailed description in [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md).\n\n## Checklist\n\n- [ ] Decision made: change wave needed?\n- [ ] Correct wave version chosen from `eng/Versions.props`\n- [ ] Wave field and `AllWaves` entry added in `ChangeWaves.cs` (if new wave)\n- [ ] Feature code wrapped with `AreFeaturesEnabled`\n- [ ] Test verifying opt-out disables the feature\n- [ ] Entry added to `documentation/wiki/ChangeWaves.md`\n","frontmatter":{"name":"changewaves","description":"Manage MSBuild Change Waves: create new waves, condition features behind opt-out flags, write tests for wave-gated features, document change waves in ChangeWaves.md, and retire expired waves. Use when adding changes that need an opt-out or rotating out old change waves. Changes that introduce a user-visible behavior change should consider whether to use a changewave.","argument-hint":"Add, query, or remove changewaves and changewave checks."},"isInternal":false,"tokens":901,"sizeBytes":4032},{"name":"SKILL.md","path":".github/skills/cswin32-com/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/cswin32-com/SKILL.md","title":"cswin32-com","category":"anthropic-skill","format":"markdown","content":"---\nname: cswin32-com\ndescription: 'Guides struct-based COM interop in MSBuild using CsWin32 patterns. Consult when working with the generic ComScope type, ComClassFactory, IComIID, the IID.Get generic method, delegate* unmanaged vtables, CoCreateInstance, or manually defining COM interfaces not in Win32 metadata (e.g. WMI IWbemLocator, IWbemServices).'\nargument-hint: 'Describe the COM interface or activation pattern you are working with.'\n---\n\n# CsWin32 COM Interop Guide\n\nStruct-based COM interop using CsWin32 patterns — AOT-compatible, no `[ComImport]` or built-in marshalling.\n\n**Paired skill:** [cswin32-interop](../cswin32-interop/SKILL.md) covers general P/Invoke and the blittable signature rules that apply to both `[DllImport]` and COM vtables. This file covers only the COM-specific layer.\n\n## Workflow\n\n1. **Interface in Win32 metadata?** Add the name to `src/Framework/NativeMethods.txt` → CsWin32 generates it. Not in metadata (WMI, Fusion, Setup Configuration) → define a manual struct under its own folder, excluded from source builds via `<Compile Remove>`.\n2. **Lifetime: `using ComScope<T> scope = new();`** for every transient COM pointer (`CoCreateInstance`, `QueryInterface`, `IEnumXxx::Next`, factory output, app-local `STDAPI Get*`, etc.). Never write `T* local; try { ... } finally { local->Release(); }` — that's the pre-`ComScope` shape that leaks on every early return. Same for `BSTR` out-params: `using BSTR x = default;`.\n3. **Activate** via `ComClassFactory.TryCreate(CLSID, ...)` (AOT-compatible) or `PInvoke.CoCreateInstance` with `IID.Get<T>()` — not `&localGuid`.\n4. **Call** via `scope.Pointer->Method(...)`. Pass `ComScope<T>` directly where the API expects `T**` / `void**` — the implicit operator does the address-of.\n5. **Match the caller's error contract.** If the top-level consumer swallows COM failure (\"no result\" == \"absent\"), helpers return `default` / `false` instead of throwing a `COMException` that's immediately discarded. `ThrowOnFailure` only when the exception will actually propagate or assert a should-never-happen.\n6. **Guard with `#if FEATURE_WINDOWSINTEROP`** — add `&& NET` only when the struct uses the static-abstract `IComIID` form exclusively (no net472 dual-target).\n\n## Manual COM Structs (Not in Metadata)\n\nDefine each interface in its own file under e.g. `src/Tasks/AssemblyDependency/Fusion/`, `src/Framework/Shared/VisualStudio/`, `src/Framework/Utilities/Wmi/`. Exclude from source builds. Pattern (dual-target — works on net472 + .NET):\n\n```csharp\ninternal unsafe struct IAssemblyCache : IComIID\n{\n    public static readonly Guid IID_IAssemblyCache = new(0xE707DCDE, ...);\n\n#if NET\n    static ref readonly Guid IComIID.Guid\n    {\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        get => ref Unsafe.AsRef(in IID_IAssemblyCache);\n    }\n#else\n    readonly ref readonly Guid IComIID.Guid\n    {\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        get => ref Unsafe.AsRef(in IID_IAssemblyCache);\n    }\n#endif\n\n    private readonly void** _lpVtbl;\n\n    // IUnknown at indices 0-2, then interface methods at 3+\n    public HRESULT QueryInterface(Guid* riid, void** ppvObject)\n    {\n        fixed (IAssemblyCache* pThis = &this)\n            return ((delegate* unmanaged[Stdcall]<IAssemblyCache*, Guid*, void**, HRESULT>)_lpVtbl[0])(pThis, riid, ppvObject);\n    }\n    // AddRef @1, Release @2, then interface methods at correct slot indices.\n}\n```\n\n**Rules:**\n- `delegate* unmanaged[Stdcall]` for the function-pointer cast — IDL `STDMETHODCALLTYPE` ⇒ `__stdcall` on Win32. Picking the wrong convention silently corrupts the stack.\n- `IComIID` has two shapes: static-abstract (`static ref readonly Guid IComIID.Guid`) on .NET 7+, instance (`readonly ref readonly Guid IComIID.Guid`) on net472 / netstandard2.0. CsWin32 picks the right one for its generated structs automatically (see [IComIID across TFMs](#icomiid-across-tfms)); a **manual** struct must spell out both arms itself. For .NET-only structs (WMI), drop the `#else` branch and gate the whole file `#if NET`. For dual-target structs (Fusion), keep both as shown — see `src/Tasks/AssemblyDependency/Fusion/` for the canonical example.\n- Use CsWin32-generated `PCWSTR` / `PWSTR` for wide strings (add to `NativeMethods.txt`); raw `char*` only when no typed equivalent exists.\n- Vtable slots are exact: index 0 = `QueryInterface`, 1 = `AddRef`, 2 = `Release`, 3+ = interface methods in IDL order. When inheriting, **add the parent interface's method count** before counting your own (e.g. `ISetupConfiguration2.EnumAllInstances` is at slot 6 = 3 IUnknown + 3 v1 + 0).\n- CS0592 prevents `[SupportedOSPlatform]` on structs — put it on individual methods.\n\n## Lifetime & Access\n\n**A raw `T*` (COM struct) is forbidden as a field on a non-`ref` type.** Allowed locations: locals in `unsafe` methods, parameters, fields of a `ref struct`. Anywhere else (instance fields of `class` / non-`ref` `struct`) → use `AgileComPointer<T>` — a raw pointer field is an apartment-agility hazard and leaks on finalize-without-Dispose.\n\n### `ComScope<T>` — transient pointers\n\n`ref struct`, `using`'d, calls `Release` on dispose. **The default for everything that doesn't survive the current method.** Implicitly converts to `T**` and `void**`, so out-params write into the scope directly:\n\n```csharp\nusing ComScope<ISomeInterface> scope = new();\nGuid clsid = SomeStruct.CLSID;\nGuid iid = IID.Get<ISomeInterface>();\nPInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &iid, scope).ThrowOnFailure();\nscope.Pointer->DoThing(...);\n\nusing ComScope<IOther> other = new();\nGuid otherIid = IOther.IID_IOther;\nscope.Pointer->QueryInterface(&otherIid, other).ThrowOnFailure();\n```\n\n- Access methods via `scope.Pointer->Method(...)`. Null-check with `scope.IsNull`.\n- Applies to *every* COM out-param: `CoCreateInstance`, `QueryInterface`, `IEnumXxx::Next`, factory methods, app-local `STDAPI Get*` (declare the `[DllImport]` with `T** pp`, not `out T*`, so the implicit operator binds).\n\n**Ownership transfer out of a helper.** A helper that acquires a COM pointer can return `ComScope<T>` directly; intermediate pointers stay in their own `using` scopes and Release on the helper's `return`. `default` `ComScope<T>` is null and its `Dispose` is a no-op, so callers don't need an extra null guard:\n\n```csharp\nprivate static ComScope<ISetupConfiguration2> AcquireSetupConfig()\n{\n    Guid clsid = SetupConfiguration.CLSID_SetupConfiguration;\n    Guid iid = ISetupConfiguration2.IID_ISetupConfiguration2;\n\n    ComScope<ISetupConfiguration2> config = new();\n    HRESULT hr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &iid, config);\n    if (hr.Succeeded) return config;                 // ownership flows to caller\n    if (hr != HRESULT.REGDB_E_CLASSNOTREG) return default;\n\n    using ComScope<ISetupConfiguration> v1 = new();  // intermediate, Released on return\n    if (SetupConfiguration.GetSetupConfiguration(v1, IntPtr.Zero) < 0 || v1.IsNull) return default;\n    return v1.Pointer->QueryInterface(&iid, config).Succeeded ? config : default;\n}\n\n// caller:\nusing ComScope<ISetupConfiguration2> config = AcquireSetupConfig();\nif (!config.IsNull) { /* use config.Pointer */ }\n```\n\n### `BSTR` — COM strings\n\nCsWin32-generated; extended in [`Windows/Win32/Foundation/BSTR.cs`](../../../src/Framework/Windows/Win32/Foundation/BSTR.cs) with `IDisposable`. `Dispose` calls `SysFreeString` and zeroes the field in place. **Scope every COM `BSTR` out-param with `using BSTR x = default;`** — no manual `try/finally + SysFreeString`:\n\n```csharp\nusing BSTR versionBstr = default;\nif (instance->GetInstallationVersion(&versionBstr).Failed) return false;\nstring version = versionBstr.ToString();\n// SysFreeString runs at scope exit.\n```\n\nIn-place-only: `BSTR.Dispose` writes through `Unsafe.AsRef(in this)`, so `using` must be on the storage location, not on a method-returned copy. Same rule as `ComScope<T>`.\n\n### `AgileComPointer<T>` — fields outliving a method\n\nFinalizable managed class. Use whenever a COM pointer is stored in a class field (the only legal way — raw `T*` fields are forbidden, see top of section).\n\n- Registers in the Global Interface Table (thread-agile); finalizer releases if `Dispose` is missed.\n- Access via `using ComScope<T> scope = agile.GetInterface();` then `scope.Pointer->Method(...)`. `GetInterface()` round-trips through the GIT — hoist a single scope to the top of a method when several calls share it.\n- Constructor `takeOwnership`: pass `false` when the raw pointer is also held by a `ComScope<T>` that will Release (GIT AddRefs independently); pass `true` only when no other code path will Release.\n- Dispose via the owner's `Dispose` / `DisposeManagedResources` (`AgileComPointer` is managed, not unmanaged — its finalizer is the safety net, not the primary cleanup).\n\n## Activation\n\n```csharp\n// AOT-compatible\nif (ComClassFactory.TryCreate(IWbemLocator.CLSID, out var factory, out HRESULT hr))\n    using ComScope<IWbemLocator> instance = factory.TryCreateInstance<IWbemLocator>(out hr);\n\n// CoCreateInstance — IID.Get<T>(), not &localGuid\nGuid clsid = IWbemLocator.CLSID;\nusing ComScope<IWbemLocator> locator = new();\nhr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.Get<IWbemLocator>(), locator);\n```\n\n## Error-Handling Parity When Migrating\n\nStruct-based COM returns raw `HRESULT`; `[ComImport]` and built-in activation threw automatically. Preserve the old throw-vs-return at each call site:\n\n| Old shape | Threw via | Migrated shape |\n|---|---|---|\n| `new SomeCoClass()` | built-in activation | `PInvoke.CoCreateInstance(...).ThrowOnFailure()` |\n| `(IFoo)rcw` cast | `InvalidCastException` on QI | `QueryInterface(&iid, scope).ThrowOnFailure()` |\n| `[ComImport]` method, default `PreserveSig=false` | marshaller throws on `FAILED(hr)` | `.ThrowOnFailure()` at the call site |\n| `[ComImport]` method with `[PreserveSig]` | caller inspects return | mirror the existing `hr` branch — don't start throwing |\n\n**Factory-method exception:** when the old contract was \"return null for invalid input\" (e.g. `Create(path)`), keep null-return only for the legitimate-rejection path; activation / QI failures still throw. See [`MetadataReader.cs`](../../../src/Tasks/ManifestUtil/MetadataReader.cs) (throws on activation/QI, returns null on `OpenScope`).\n\n**Don't throw when the caller swallows it.** If the top-level consumer wraps the whole chain in `catch (COMException) { }`, inner helpers must return `default` / `false` / empty instead of constructing a `COMException` only to have it discarded. Throwing here allocates the exception, walks the stack, and hides the HRESULT for no benefit. See [`VisualStudioLocationHelper.AcquireSetupConfiguration2`](../../../src/Framework/VisualStudioLocationHelper.cs).\n\n## Mocking Struct-Based COM in Tests\n\nStruct-based COM calls go through raw vtable pointers, so a managed mock can't be passed where a `T*` is expected. Bridge with the **built-in COM marshaller**: the mock implements the *managed* interface (BCL `System.Runtime.InteropServices.ComTypes.*` when ABI-compatible — true for `ITypeLib` / `ITypeInfo` — else CsWin32's nested `[ComImport] internal interface Interface`), and the test passes a **CCW** (COM-callable wrapper) pointer to the code under test:\n\n```csharp\n// MockTypeLib : ComTypes.ITypeLib   (a normal managed class)\nIntPtr ccw = Marshal.GetComInterfaceForObject(mock, typeof(ComTypes.ITypeLib));\ntry { walker.AnalyzeTypeLibrary((ITypeLib*)ccw); }   // struct API; real QueryInterface works\nfinally { Marshal.Release(ccw); }\n```\n\nThe CCW exposes a real vtable, so no hand-rolled native vtable is needed and the managed mock framework stays intact. But the mock must now behave like real COM, not a managed object:\n\n- **All `[out]` params are written**, even ones the caller ignores. Passing `null` for a trailing out-param (`GetDocumentation(-1, &name, null, null, null)`) faults the marshaller — supply a real pointer for each.\n- **Exception identity is lost.** An HRESULT-returning method resurfaces a thrown exception as a *new* `COMException` (HRESULT preserved, instance not); a `uint`/`void` method with no HRESULT channel (`GetTypeInfoCount`, the `Release*` methods) **swallows it silently** and returns garbage. Assert on type/count, not the injected instance, and don't rely on `void`/`uint` methods to propagate.\n- **Throw `COMException` on bad input, don't `Assert`.** An `Assert` throws an `XunitException` that escapes the SUT's `catch (COMException)`; a `COMException` becomes a failing HRESULT the SUT handles normally. (Matters because a swallowed `GetTypeInfoCount` failure can feed an out-of-range index to the next call.)\n\nSee [`MockTypeLib.cs`](../../../src/Tasks.UnitTests/MockTypeLib.cs) / [`MockTypeInfo.cs`](../../../src/Tasks.UnitTests/MockTypeInfo.cs) and the CCW bridge in [`ComReferenceWalker_Tests.cs`](../../../src/Tasks.UnitTests/ComReferenceWalker_Tests.cs).\n\n## Strongly-typed handle / token wrappers\n\nWhen the native side `typedef`s a primitive into a family of \"same shape, different meaning\" aliases (`corhdr.h`'s `typedef ULONG32 mdToken; typedef mdToken mdAssembly; ...`), mirror that hierarchy with distinct `readonly struct` wrappers holding the single underlying primitive — same pattern as CsWin32's `HANDLE` / `HWND` / `HMODULE`. Stays blittable; `delegate*` casts and arrays marshal at zero cost.\n\nConversions follow the typedef hierarchy: **implicit** widening to the base (`MdAssembly` → `MdToken`, always safe), **explicit** narrowing (`(MdAssembly)token`, opt-in because the C side can't enforce the kind at the cast site).\n\n**Check the native header for the canonical validation primitives** before defining `IsNil` / `IsValid`. Encoding details often hide in macros that don't grep cleanly. For `mdToken` the encoding is `(TableType << 24) | Rid`:\n\n| C macro | Meaning |\n|---|---|\n| `TypeFromToken(tk) = tk & 0xff000000` | Table-type tag (high byte) → `CorTokenType` |\n| `RidFromToken(tk) = tk & 0x00ffffff`  | Row id (low 24 bits) |\n| `IsNilToken(tk) = RidFromToken(tk) == 0` | Nil check — **rid half, not the whole value** |\n| `mdAssemblyNil = mdtAssembly = 0x20000000` | Per-type nil is the table-type tag, **not 0** |\n\nA naive `IsNil => Value == 0` would misclassify the `0x20000000` \"no assembly in this scope\" return from `GetAssemblyFromScope`. Mirror the macro: `IsNil => Rid == 0`. Reference: [`Tokens.cs`](../../../src/Tasks/AssemblyDependency/Metadata/Tokens.cs), [`CorTokenType.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorTokenType.cs).\n\n## IComIID across TFMs\n\nCsWin32 (since **0.3.287**) emits `IComIID` and attaches it to every generated COM struct on **all** target frameworks — static-abstract `static ref readonly Guid Guid { get; }` on .NET 7+, instance `ref readonly Guid Guid { get; }` on net472 / netstandard2.0. No hand-authored polyfill is needed; the old `src/Framework/Polyfills/IComIID.cs` + `IComIIDPolyfills.cs` files (one `partial` per generated struct) are gone.\n\nWhen you add a new CsWin32-generated COM type to `NativeMethods.txt`, it carries `IComIID` automatically through `ComScope<T>` — nothing extra to do.\n\nOnly **manual** structs (WMI, Fusion, Setup Configuration, CLR metadata) implement `IComIID` by hand, matching the per-TFM shape — see [Manual COM Structs](#manual-com-structs-not-in-metadata). `IID.Get<T>()` ([`IID.cs`](../../../src/Framework/Windows/Win32/IID.cs)) reads it via `T.Guid` on .NET and `default(T).Guid` on net472.\n\n## File Organization\n\n| Location | Contents |\n|---|---|\n| `src/Framework/Windows/Win32/System/Com/` | `ComScope`, `ComClassFactory`, `AgileComPointer`, `GlobalInterfaceTable` |\n| `src/Framework/Windows/Win32/IID.cs` | Generic IID lookup |\n| `src/Framework/Utilities/Wmi/`, `src/Framework/Shared/VisualStudio/`, `src/Tasks/AssemblyDependency/Fusion/`, `src/Tasks/AssemblyDependency/Metadata/`, `src/Tasks/TypeLib/` | Manual COM struct interfaces — own folder per API surface |\n\n## CS3016 CLS Compliance\n\nCsWin32 (since **0.3.287**) auto-emits `[CLSCompliant(false)]` on every generated COM struct wrapper that carries CCW thunks (the `[UnmanagedCallersOnly(CallConvs = new[] { ... })]` array argument is what trips CS3016), and adds CS3019 / CS3021 to its own generated-file `#pragma warning disable` list. So a CLS-compliant assembly (`[assembly: CLSCompliant(true)]`) builds clean with no consumer action — the hand-authored `GeneratedInteropClsCompliance.cs` partial and the `.editorconfig` CS3019 suppression for `{**/Windows/**/*.cs}` are gone.\n\nThe attribute is only emitted on .NET 5+ TFMs (net472 / netstandard2.0 have no CCW thunks, so no CS3016) and only for `internal` wrappers (the generator default). Don't re-add per-type `[CLSCompliant(false)]` partials or blanket `NoWarn`. See https://github.com/dotnet/roslyn/issues/68526 for the underlying Roslyn limitation that motivated the generator-side fix.\n","frontmatter":{"name":"cswin32-com","description":"Guides struct-based COM interop in MSBuild using CsWin32 patterns. Consult when working with the generic ComScope type, ComClassFactory, IComIID, the IID.Get generic method, delegate* unmanaged vtables, CoCreateInstance, or manually defining COM interfaces not in Win32 metadata (e.g. WMI IWbemLocator, IWbemServices).","argument-hint":"Describe the COM interface or activation pattern you are working with."},"isInternal":false,"tokens":4380,"sizeBytes":17016},{"name":"SKILL.md","path":".github/skills/cswin32-interop/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/cswin32-interop/SKILL.md","title":"cswin32-interop","category":"anthropic-skill","format":"markdown","content":"---\nname: cswin32-interop\ndescription: 'Guides CsWin32 P/Invoke interop in MSBuild. Consult when working with the PInvoke class, Windows.Win32 namespaces, FEATURE_WINDOWSINTEROP, HANDLE/HMODULE/HRESULT types, the generic BufferScope type, replacing [DllImport] with CsWin32, or conditioning Windows-only code for source builds.'\nargument-hint: 'Describe the Windows API or interop code you are migrating or adding.'\n---\n\n# CsWin32 Interop Guide\n\n[CsWin32](https://github.com/microsoft/CsWin32) replaces `[DllImport]` with source-generated `PInvoke.*` calls. `FEATURE_WINDOWSINTEROP` is the compile-time gate; source builds disable it.\n\n**Paired skill:** [cswin32-com](../cswin32-com/SKILL.md) covers struct-based COM interop on top of CsWin32 (`ComScope<T>`, `AgileComPointer<T>`, `delegate* unmanaged` vtables, `IComIID`, `CoCreateInstance`, manual COM structs not in Win32 metadata). This file covers only the general P/Invoke layer; the COM skill builds on its blittable-signature rules.\n\n## Rules\n\n1. **Replace `[DllImport]` with `PInvoke.*`**. Delete old declarations and hand-written structs/enums/constants.\n2. **Gate with `#if FEATURE_WINDOWSINTEROP`**, add runtime `IsWindows` check inside. Both required.\n3. **Use CsWin32 types directly** (`HANDLE`, `HMODULE`, `HRESULT.S_OK`, `FILE_FLAGS_AND_ATTRIBUTES`, etc.).\n4. **Call `PInvoke.*` directly** — no wrappers. Types flow via `InternalsVisibleTo`.\n5. **Prefer CsWin32 for Windows APIs**. Use `[LibraryImport]` only for non-Windows native calls (e.g. `libc`), guarded with `#if NET`.\n6. **Preserve the old error-handling contract.** Check the original `[DllImport]` for `PreserveSig` / `SetLastError` / `BOOL` / `HRESULT` semantics and reproduce them: `PreserveSig=false` → `.ThrowOnFailure()`; `SetLastError=true` + failed `BOOL` → `throw new Win32Exception()`. Silently returning where the old code threw is a behavior change. See [cswin32-com's parity table](../cswin32-com/SKILL.md#error-handling-parity-when-migrating) for the COM-side equivalent.\n\n## Blittable signatures\n\nCsWin32 is configured with `allowMarshaling: false`, so every `[DllImport]` and every manual\nCOM vtable method must be blittable — no marshalling at the boundary. These rules apply\nto both. For COM-vtable-only additions, see\n[cswin32-com](../cswin32-com/SKILL.md#blittable-vtable-signatures).\n\n- **Return `HRESULT`** from HRESULT-returning APIs (not `int`). Blittable (single `int` field),\n  exposes `.Succeeded` / `.Failed` / `.ThrowOnFailure()`. Use `HRESULT.S_OK` over `0`; cast\n  `e.HResult` to `(HRESULT)` when wrapping. `AddRef` / `Release` return `uint`.\n- **Call `.ThrowOnFailure()`** instead of `if (hr.Failed) Marshal.ThrowExceptionForHR(hr)` —\n  same exception, IErrorInfo-enriched, one-line call site:\n  `iface->Method(...).ThrowOnFailure();`. Branch on `hr` only when handling a specific HRESULT\n  (e.g. `ERROR_INSUFFICIENT_BUFFER`) before throwing. See\n  [cswin32-com's parity table](../cswin32-com/SKILL.md#error-handling-parity-when-migrating)\n  for the migration contract.\n- **Use `PCWSTR` / `PWSTR`** for wide strings, never managed `string`. Implicit conversion from\n  `fixed (char* p = managedString)`. Add to `NativeMethods.txt` if not yet generated.\n- **`T**` not `out T*`** for pointer outputs. `out` triggers marshaling + a `fixed` round-trip\n  at every call site.\n- **`void*` for opaque / reserved params** — never `IntPtr.Zero`; pass `null` literally.\n  `IntPtr` is fine at boundaries with the wider .NET surface (`Marshal.*`,\n  `SafeHandle.DangerousGetHandle`, public API).\n- **Prefer `nint` / `nuint`** over `IntPtr` / `UIntPtr` for native-sized integers — no boxing,\n  better cast semantics, no `IntPtr.Zero` ceremony.\n- **No managed reference types** (`string`, `StringBuilder`, arrays) in blittable signatures.\n- **Don't specify `PreserveSig = true` on `[DllImport]`** — it's the default. Use\n  `PreserveSig = false` only to force marshaller throw-on-failure (rare; prefer returning\n  `HRESULT` and `.ThrowOnFailure()`). `[ComImport]` defaults the opposite way, but struct-based\n  COM uses raw `delegate*` and isn't affected — see\n  [cswin32-com](../cswin32-com/SKILL.md#blittable-vtable-signatures).\n- **Constrain flag / option parameters to a typed `enum`.** When a native `DWORD` / `ULONG` /\n  `int` is documented as a `typedef enum` or `#define` set, declare a C# `[Flags] enum Foo : uint`\n  (matching the underlying type) and use it in the signature (and `delegate*` cast for COM).\n  Mirror the constraint even when the native side has no named enum. Self-documenting at the\n  call site: `OpenScope(path, CorOpenFlags.ofRead, ...)` vs `OpenScope(path, 0, ...)`. Co-locate\n  the enum next to its consumer. See\n  [`CorOpenFlags.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorOpenFlags.cs) and\n  [`CorAssemblyFlags.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorAssemblyFlags.cs).\n\n### Dual Guard Pattern\n\n```csharp\n#if FEATURE_WINDOWSINTEROP\n    if (IsWindows)\n    {\n        PInvoke.GetFileAttributesEx(fullPath, out WIN32_FILE_ATTRIBUTE_DATA data);\n    }\n#endif\n    // Cross-platform fallback\n```\n\n**WRONG**: `if (IsWindows) { #if FEATURE_WINDOWSINTEROP ... #endif }` — dead code in source builds.\n\nWindows-only files are excluded via `<Compile Remove>` instead — no `#if` inside needed.\n\n## Infrastructure\n\n**Define**: `src/Directory.BeforeCommon.targets` sets `FEATURE_WINDOWSINTEROP` + `$(FeatureWindowsInterop)` when `DotNetBuildSourceOnly != true`. Use `$(FeatureWindowsInterop)` in `.csproj` for `<Compile Remove>`/`<Compile Include>`.\n\n**CsWin32 config**: `src/Framework/NativeMethods.txt` (API list) + `NativeMethods.json` (`allowMarshaling: false`, `useSafeHandles: false`). Lives in Framework; other projects consume via `InternalsVisibleTo`. Do not add CsWin32 to other projects.\n\n**Guard selection**:\n\n| Guard | When | Runtime check? |\n|-------|------|----------------|\n| `#if FEATURE_WINDOWSINTEROP` | Multi-TFM Windows calls | Yes |\n| `#if FEATURE_WINDOWSINTEROP && NET` | Manual COM structs gated .NET-only (e.g. WMI). CsWin32-generated COM types via `ComScope<T>` work on net472 too — the generator emits `IComIID` on every TFM (see [cswin32-com](../cswin32-com/SKILL.md#icomiid-across-tfms)) | Yes |\n| `#if FEATURE_WINDOWSINTEROP && !NETSTANDARD` | CsWin32 types without `static abstract` (net472 + net10) | Yes |\n| `#if !NET` / `#if FEATURE_MSCOREE` | net472-only = inherently Windows | No |\n\n**Namespace imports** must be inside `#if FEATURE_WINDOWSINTEROP`. WDK APIs use `Windows.Wdk` namespace.\n\n**Files**: `src/Framework/Windows/` (CsWin32 partials), `src/Shared/Win32/` (COM helpers), `src/Framework/Utilities/Wmi/` (.NET-only COM structs).\n\n### Constant Replacements\n\n`NativeMethodsShared.S_OK` → `HRESULT.S_OK`, `InvalidHandle` → `HANDLE.INVALID_HANDLE_VALUE`, `FILE_ATTRIBUTE_DIRECTORY` → `FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY`, `STD_OUTPUT_HANDLE` → `STD_HANDLE.STD_OUTPUT_HANDLE`, `GENERIC_READ` → `FILE_ACCESS_RIGHTS.FILE_GENERIC_READ`. Pattern: `CsWin32EnumType.ORIGINAL_NAME` — check generated types in `obj/`.\n\n**Always prefer the generated enum over a local copy.** Before defining `private const int ERROR_*` / `private enum FooFlags`, grep the CsWin32 metadata:\n\n- `ERROR_*` (Win32 error codes) → `WIN32_ERROR.ERROR_*` (uint enum)\n- `HRESULT` codes → `HRESULT.S_OK`, etc.\n- Restart Manager status/type → `RM_APP_STATUS` / `RM_APP_TYPE`\n- File flags → `FILE_FLAGS_AND_ATTRIBUTES`, `FILE_ACCESS_RIGHTS`, `FILE_SHARE_MODE`, `FILE_CREATION_DISPOSITION`\n- Process flags → `PROCESS_CREATION_FLAGS`, `STARTUPINFOW_FLAGS`, `PROCESS_ACCESS_RIGHTS`\n- Memory mapping → `PAGE_PROTECTION_FLAGS`, `FILE_MAP`\n- Shell folder → `KNOWN_FOLDER_FLAG`\n\nCast `int`/`uint` return codes via `(WIN32_ERROR)res` for `switch` and equality. Add the enum to `NativeMethods.txt` if not yet generated, then check `obj/.../generated/Microsoft.Windows.CsWin32/.../Windows.Win32.<EnumName>.g.cs`.\n\n**Some flag values are standalone constants, not enum members.** A Win32 `#define` outside a `typedef enum` generates as an `internal const` on `PInvoke`. Example: the `LoadTypeLibEx` flags are `PInvoke.LOAD_TLB_AS_32BIT` / `_64BIT` (`uint`), **not** members of `REGKIND` (which has only `REGKIND_DEFAULT/REGISTER/NONE`). Add the *constant name* to `NativeMethods.txt` like any API. OR it onto an enum at the constant's width and cast back: `(REGKIND)((uint)REGKIND.REGKIND_NONE | PInvoke.LOAD_TLB_AS_32BIT)`. Don't reintroduce a local `const` CsWin32 already emits.\n\n**Match local types to the CsWin32 type.** Instead of `int res = (int)PInvoke.RmStartSession(...)` and casting at every comparison, declare `WIN32_ERROR res = PInvoke.RmStartSession(...)` and let helpers like `GetException(WIN32_ERROR res, ...)` take the typed value. Cast to `int`/`uint` only at the boundary where a non-CsWin32 API needs it (e.g. `new Win32Exception((int)res, ...)`). The same applies to `HRESULT`, `BOOL`, `HANDLE`, `PROCESS_CREATION_FLAGS`, etc.\n\n**Delete local mirror enums** that exist solely to mirror the Win32 one. The generated CsWin32 type is the source of truth.\n\n### FILETIME Conversions\n\nUse the helpers in [`src/Framework/Windows/Win32/Foundation/FileTimeExtensions.cs`](../../../src/Framework/Windows/Win32/Foundation/FileTimeExtensions.cs):\n\n- `fileTime.ToLong()` → 64-bit ticks\n- `fileTime.ToDateTime()` → local `DateTime` (FILETIME values returned as local time, e.g. `RM_PROCESS_INFO.ProcessStartTime`)\n- `fileTime.ToDateTimeUtc()` → UTC `DateTime` (FILETIME values returned as UTC, e.g. `WIN32_FILE_ATTRIBUTE_DATA.ftLastWriteTime`)\n\nDo **not** hand-roll `DateTime.FromFileTime((long)hi << 32 | lo)` — use the helpers for consistency. Note CsWin32-generated structs use `ComTypes.FILETIME` (int fields) for COM members and `Windows.Win32.Foundation.FILETIME` (uint fields) for kernel ones; the extension covers `ComTypes.FILETIME`.\n\n## BufferScope<T>\n\n`BufferScope<T>` (`src/Framework/Utilities/BufferScope.cs`) — stackalloc initial buffer with `ArrayPool<T>` fallback. Lives in Framework, available to all projects via `InternalsVisibleTo`.\n\n```csharp\nusing BufferScope<char> buffer = new(stackalloc char[(int)PInvoke.MAX_PATH]);\nint length = (int)PInvoke.GetShortPathName(path, buffer.AsSpan());\nif (length > buffer.Length)\n{\n    buffer.EnsureCapacity(length);\n    length = (int)PInvoke.GetShortPathName(path, buffer.AsSpan());\n}\nif (length > 0) path = buffer.Slice(0, length).ToString();\n```\n\n- `ref struct` — always use with `using`. Never stack-allocate more than 1024 bytes.\n- Check CsWin32 convenience overloads (e.g. `GetShortPathName(string, Span<char>)`) before writing `fixed` blocks.\n\n## Gotchas\n\n### CA1416 Platform Compatibility\n\nNo blanket `NoWarn` — handle semantically:\n- `if (IsWindows)` satisfies `[SupportedOSPlatform]` — no pragma needed\n- `if (IsUnixLike)` satisfies `[UnsupportedOSPlatform(\"windows\")]`\n- **Never use `!IsWindows`** — use `else if (IsUnixLike)`. See `documentation/specs/CA1416-analyzer-analysis.md`\n- Use versioned `[SupportedOSPlatform(\"windows6.1\")]` on methods calling CsWin32 APIs\n- `#pragma warning disable CA1416` only for **static local functions** (analyzer limitation)\n- CS0592 prevents `[SupportedOSPlatform]` on `partial struct` — put on individual members instead\n\n### Type Conversions\n\n- `HANDLE ↔ IntPtr`: `(HANDLE)intPtr` / `(IntPtr)h.Value`. Sentinels: `HANDLE.Null`, `HANDLE.INVALID_HANDLE_VALUE`\n- `FILETIME` conversion: `data.ftLastWriteTime.ToLong()`, `.ToDateTime()` (local), `.ToDateTimeUtc()` — see \"FILETIME Conversions\" above. CsWin32 uses `ComTypes.FILETIME` (int fields), not `Win32.Foundation.FILETIME`\n- `SafeFileHandle`: `new SafeFileHandle((IntPtr)h.Value, true)`, pass with `(HANDLE)handle.DangerousGetHandle()`\n- Nullable structs: `(SECURITY_ATTRIBUTES?)null`\n- Enum flags: use bitwise `&` — `HasFlag()` boxes on .NET Framework\n- Anonymous unions: `systemInfo.Anonymous.Anonymous.wProcessorArchitecture` — check generated source in `obj/`\n\n### Source-Build Verification (REQUIRED before pushing)\n\nSource builds (`DotNetBuildSourceOnly=true`) disable `FEATURE_WINDOWSINTEROP`. CI treats **all warnings as errors**. Run both builds before every push:\n\n```shell\n# Normal build\ndotnet msbuild MSBuild.Dev.slnf -v:q\n\n# Source-build — catches unused usings/members/docs from #if guards\ndotnet msbuild MSBuild.SourceBuild.slnf /p:DotNetBuildSourceOnly=true -v:q\n```\n\n**Everything** only referenced inside `#if FEATURE_WINDOWSINTEROP` must also be guarded:\n- **IDE0005**: `using` directives — most common failure\n- **IDE0051/IDE0052**: Private members (methods, fields, including helpers like `StringToByteArray`, constants like `ERROR_SHARING_VIOLATION`)\n- **CA1823**: Unused private fields (e.g. constants only consumed inside the guard)\n- **CS1587**: XML doc comments (move inside `#if`, not before)\n\nThe same applies when adding **polyfills** in `src/Framework/Polyfills/` (e.g. `SpanExtensions`, `IndexOfAnyExcept`): polyfills usually live behind `#if !NET` (or similar TFM guards) but are still consumed from `#if FEATURE_WINDOWSINTEROP` code paths. Always run the source-build to confirm the polyfill, its callers, and any helper members compile cleanly when interop is disabled — a polyfill referenced only by Windows-only code will trip IDE0051/CA1823 in source-only builds.","frontmatter":{"name":"cswin32-interop","description":"Guides CsWin32 P/Invoke interop in MSBuild. Consult when working with the PInvoke class, Windows.Win32 namespaces, FEATURE_WINDOWSINTEROP, HANDLE/HMODULE/HRESULT types, the generic BufferScope type, replacing [DllImport] with CsWin32, or conditioning Windows-only code for source builds.","argument-hint":"Describe the Windows API or interop code you are migrating or adding."},"isInternal":false,"tokens":3532,"sizeBytes":13339},{"name":"SKILL.md","path":".github/skills/deploy-msbuild-to-vs/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/deploy-msbuild-to-vs/SKILL.md","title":"deploy-msbuild-to-vs","category":"anthropic-skill","format":"markdown","content":"---\nname: deploy-msbuild-to-vs\ndescription: Deploy locally-built MSBuild binaries into a Visual Studio installation for testing and debugging. Use when you need VS itself to use your local MSBuild changes, or when debugging MSBuild as invoked by VS.\nargument-hint: Deploy local MSBuild to VS, patch VS with built MSBuild, debug MSBuild inside VS.\n---\n\n# Deploying Built MSBuild into Visual Studio\n\nThis skill guides you through replacing Visual Studio's bundled MSBuild with your locally-built binaries, so that builds initiated from within VS use your changes. It also covers debugging MSBuild when invoked by VS and restoring the original state.\n\n## When to Use\n\n- You need Visual Studio's build (Ctrl+Shift+B, Build Solution, design-time builds) to use your local MSBuild changes.\n- You are debugging an issue that only reproduces when MSBuild is called through the VS process (`devenv.exe`).\n- You want to test MSBuild changes against a real VS solution end-to-end.\n\n**If you only need command-line testing**, prefer the bootstrap approach instead (see the `use-bootstrap-msbuild` skill). It is faster and does not risk breaking your VS installation.\n\n## Prerequisites\n\n- Windows (the deploy script targets VS on Windows).\n- The latest Visual Studio 2026 installed with the components listed in [`.vsconfig`](../../../.vsconfig).\n- .NET Framework 3.5 installed on the Windows machine.\n- An **administrator** PowerShell prompt (required to write into `Program Files`).\n- The MSBuild repo cloned and buildable.\n\n## Step 1: Build MSBuild\n\nRun a full build from the repo root to produce the bootstrap output:\n\n```powershell\n.\\build.cmd\n```\n\nBy default this builds `Debug` configuration. If you want `Release`:\n\n```powershell\n.\\build.cmd /p:Configuration=Release\n```\n\nThe build creates bootstrap binaries under `artifacts\\bin\\MSBuild.Bootstrap\\{configuration}\\`.\n\n## Step 2: Locate Your VS MSBuild Bin Folder\n\nFind the MSBuild `Bin` folder inside your VS installation. The path depends on your VS version and edition.\n\n**⚠ Important**: If you have multiple VS versions or editions installed side-by-side, make sure you identify the correct installation path for the instance you want to patch. Confirm the path exists before deploying.\n\nThe general pattern is:\n\n```text\nC:\\Program Files\\Microsoft Visual Studio\\{version}\\{edition}\\MSBuild\\Current\\Bin\n```\n\nFor Visual Studio 2026, `{version}` is `18` (not `2026`). Common `{edition}` folders include `Enterprise`, `Professional`, and `Community`; this is customizable, so use `vswhere` below to confirm the path.\n\nExample paths:\n\n| VS Version | Edition | Typical Path |\n| ---------- | ------- | ------------ |\n| 2026 | Enterprise | `C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin` |\n| 2026 | Community | `C:\\Program Files\\Microsoft Visual Studio\\18\\Community\\MSBuild\\Current\\Bin` |\n\n### Finding the path automatically\n\nThe most reliable way to locate your VS installation is with `vswhere`:\n\n```powershell\n# List all VS installations with their paths\n& \"${env:ProgramFiles(x86)}\\Microsoft Visual Studio\\Installer\\vswhere.exe\" -all -prerelease -format table\n\n# Get the MSBuild Bin path for the latest installation\n$vsPath = & \"${env:ProgramFiles(x86)}\\Microsoft Visual Studio\\Installer\\vswhere.exe\" -latest -prerelease -property installationPath\n\"$vsPath\\MSBuild\\Current\\Bin\"\n```\n\nAlternatively, from a VS Developer Command Prompt:\n\n```cmd\nwhere msbuild\n```\n\nUse the directory containing that `MSBuild.exe`.\n\n## Step 3: Deploy with the Script\n\nFrom an **administrator** PowerShell, run:\n\n```powershell\n.\\scripts\\Deploy-MSBuild.ps1 -destination \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\"\n```\n\n### Script Parameters\n\n| Parameter | Default | Description |\n| --------- | ------- | ----------- |\n| `-destination` | *(required)* | The VS MSBuild `Bin` folder path |\n| `-configuration` | `Debug` | Must match the configuration used in `build.cmd` |\n| `-runtime` | `Detect` | Auto-detects `Desktop` vs `Core` from the path. Use `Desktop` for VS, `Core` for .NET SDK |\n| `-binDirectory` | `artifacts\\bin` | Override if build output is elsewhere |\n| `-makeBackup` | `$true` | Creates a timestamped `Backup-*` folder before overwriting |\n\n### What Gets Copied\n\nThe script copies from `artifacts\\bin\\MSBuild.Bootstrap\\{configuration}\\net472\\`:\n\n- **Core DLLs**: `Microsoft.Build.dll`, `Microsoft.Build.Framework.dll`, `Microsoft.Build.Tasks.Core.dll`, `Microsoft.Build.Utilities.Core.dll`, `Microsoft.NET.StringTools.dll`\n- **Targets files**: `Microsoft.Common.targets`, `Microsoft.CSharp.targets`, `Microsoft.VisualBasic.targets`, and many more\n- **Executables**: `MSBuild.exe` (x86 and amd64), `MSBuildTaskHost.exe`\n- **Framework dependencies**: Various `System.*` assemblies needed for .NET Framework\n\n## Step 4: Test in Visual Studio\n\n1. **Close and reopen** Visual Studio (it caches MSBuild on startup).\n2. Open a solution and build — VS now uses your locally-built MSBuild.\n3. Verify by checking the build output or binary log for your changes.\n\n## Restoring the Original MSBuild\n\nThe deploy script creates a backup folder at `{destination}\\Backup-{timestamp}\\` by default.\n\nTo restore:\n\n```powershell\n# Find the backup folder\nGet-ChildItem \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\\Backup-*\"\n\n# Copy everything back\nCopy-Item \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\\Backup-{timestamp}\\*\" `\n          \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\\\" -Recurse -Force\n```\n\nAlternatively, run **Visual Studio Installer → Modify → Repair** to fully restore the original binaries.\n\n⚠ **CAUTION**: If you overwrite MSBuild in Visual Studio and something goes wrong, VS itself may break (since VS uses MSBuild internally). Always keep the backup and know how to restore before deploying.\n\n## Debugging MSBuild Inside Visual Studio\n\nWhen MSBuild is invoked by VS (for real builds or design-time builds), you can attach a debugger using environment variables. Set these **before** launching VS:\n\n### Breakpoint at BuildManager.BeginBuild (Recommended for VS)\n\n```powershell\n$env:MSBuildDebugBuildManagerOnStart = \"1\"   # 1 = Debugger.Launch(), 2 = wait for attach\n$env:MSBuildDebugProcessName = \"devenv\"      # Only trigger in the VS process\ndevenv.exe\n```\n\nThis breaks into `BuildManager.BeginBuild`, which is where VS calls into MSBuild's API.\n\n### Breakpoint at MSBuild Entry Point\n\n```powershell\n$env:MSBUILDDEBUGONSTART = \"1\"   # 1 = Launch debugger, 2 = Wait for attach, 3 = Main process only\ndevenv.exe\n```\n\n### Capturing Full Binary Logs from VS\n\nVS heavily filters MSBuild events and suppresses them entirely during design-time builds. To capture everything:\n\n```powershell\n$env:MSBuildDebugEngine = \"1\"\n$env:MSBUILDDEBUGPATH = \"C:\\temp\\MSBuild_Logs\"   # Optional: override log output directory\n$env:MSBuildDebugProcessName = \"devenv\"            # Optional: only log from VS\ndevenv.exe\n```\n\nThis injects a binary log at `BuildManager.BeginBuild`, capturing full logs for both real and design-time builds. Logs are saved to `MSBuild_Logs\\` under the current directory (or `MSBUILDDEBUGPATH` if set).\n\n## Deploying to .NET SDK Instead\n\nTo patch a .NET SDK installation rather than VS:\n\n```powershell\n.\\scripts\\Deploy-MSBuild.ps1 -destination \"C:\\Program Files\\dotnet\\sdk\\10.0.100\" -runtime Core\n```\n\nThe script auto-detects SDK paths (containing `dotnet` and `sdk`), but you can force it with `-runtime Core`.\n\n## Cross-Machine Deployment\n\nIf you cannot build MSBuild on the target machine:\n\n1. Build and deploy to an **empty folder**:\n\n   ```powershell\n   .\\scripts\\Deploy-MSBuild.ps1 -destination \"C:\\temp\\msbuild-deploy\"\n   ```\n\n2. Copy the contents of that folder to the target machine's VS MSBuild Bin folder.\n3. Make a manual backup of the target machine's files first.\n\n## Troubleshooting\n\n| Problem | Solution |\n| ------- | -------- |\n| Access denied when deploying | Run PowerShell as **Administrator** |\n| VS won't start after deploy | Restore from `Backup-*` folder or run VS Installer Repair |\n| Changes not reflected in VS | Close all VS instances and reopen; kill lingering `MSBuild.exe` processes |\n| Configuration mismatch | Ensure `-configuration` matches what you passed to `build.cmd` |\n| Build errors in MSBuild repo | Run `.\\build.cmd` from a clean state; see [Something's wrong in my build](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Something's-wrong-in-my-build.md) |\n\n## Quick Reference\n\n| Step | Command |\n| ---- | ------- |\n| Build MSBuild | `.\\build.cmd` |\n| Deploy to VS (admin) | `.\\scripts\\Deploy-MSBuild.ps1 -destination \"{VS MSBuild Bin path}\"` |\n| Deploy Release build | `.\\scripts\\Deploy-MSBuild.ps1 -destination \"{path}\" -configuration Release` |\n| Debug MSBuild in VS | Set `$env:MSBuildDebugBuildManagerOnStart = \"1\"` then launch `devenv.exe` |\n| Capture VS binlogs | Set `$env:MSBuildDebugEngine = \"1\"` then launch `devenv.exe` |\n| Restore original | Copy back from `Backup-*` folder or run VS Installer Repair |\n\n## See Also\n\n- [Deploy-MSBuild documentation](https://github.com/dotnet/msbuild/blob/main/documentation/Deploy-MSBuild.md)\n- [Building and Debugging on Full Framework](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md)\n- [Bootstrap Documentation](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Bootstrap.md)\n","frontmatter":{"name":"deploy-msbuild-to-vs","description":"Deploy locally-built MSBuild binaries into a Visual Studio installation for testing and debugging. Use when you need VS itself to use your local MSBuild changes, or when debugging MSBuild as invoked by VS.","argument-hint":"Deploy local MSBuild to VS, patch VS with built MSBuild, debug MSBuild inside VS."},"isInternal":false,"tokens":2304,"sizeBytes":9480},{"name":"polyfills.md","path":".github/skills/dotnet-aot-compat/references/polyfills.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/dotnet-aot-compat/references/polyfills.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Polyfills for Older TFMs\n\n`DynamicallyAccessedMembersAttribute` shipped in .NET 5. For projects targeting netstandard2.0 or net472, you need a polyfill. The trimmer recognizes the attribute by name, so a local copy works:\n\n```csharp\n#if !NET\nnamespace System.Diagnostics.CodeAnalysis\n{\n    [AttributeUsage(AttributeTargets.Field | AttributeTargets.ReturnValue |\n                    AttributeTargets.GenericParameter | AttributeTargets.Parameter |\n                    AttributeTargets.Property, Inherited = false)]\n    internal sealed class DynamicallyAccessedMembersAttribute : Attribute\n    {\n        public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes)\n            => MemberTypes = memberTypes;\n        public DynamicallyAccessedMemberTypes MemberTypes { get; }\n    }\n\n    [Flags]\n    internal enum DynamicallyAccessedMemberTypes\n    {\n        None = 0,\n        PublicParameterlessConstructor = 0x0001,\n        PublicConstructors = 0x0002 | PublicParameterlessConstructor,\n        NonPublicConstructors = 0x0004,\n        PublicMethods = 0x0008,\n        NonPublicMethods = 0x0010,\n        PublicFields = 0x0020,\n        NonPublicFields = 0x0040,\n        PublicNestedTypes = 0x0080,\n        NonPublicNestedTypes = 0x0100,\n        PublicProperties = 0x0200,\n        NonPublicProperties = 0x0400,\n        PublicEvents = 0x0800,\n        NonPublicEvents = 0x1000,\n        Interfaces = 0x2000,\n        All = ~None // Discouraged — prefer specific flags\n    }\n}\n#endif\n```\n\nSimilarly for `RequiresUnreferencedCodeAttribute` and `UnconditionalSuppressMessageAttribute` if needed on older TFMs.\n","isInternal":false,"tokens":385,"sizeBytes":1625},{"name":"SKILL.md","path":".github/skills/dotnet-aot-compat/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/dotnet-aot-compat/SKILL.md","title":"dotnet-aot-compat","category":"anthropic-skill","format":"markdown","content":"---\nname: dotnet-aot-compat\ndescription: >\n  Make .NET projects compatible with Native AOT and trimming by systematically\n  resolving IL trim/AOT analyzer warnings. USE FOR: making projects AOT-compatible,\n  fixing trimming warnings, resolving IL warnings (IL2026, IL2070, IL2067, IL2072,\n  IL3050), adding DynamicallyAccessedMembers annotations, enabling IsAotCompatible.\n  DO NOT USE FOR: publishing native AOT binaries, optimizing binary size, replacing\n  reflection-heavy libraries with alternatives.\n  INVOKES: no tools — pure knowledge skill.\nlicense: MIT\n---\n\n# dotnet-aot-compat\n\nMake .NET projects compatible with Native AOT and trimming by systematically resolving all IL trim/AOT analyzer warnings.\n\n## When to Use This Skill\n\n- **\"Make this project AOT-compatible\"**\n- **\"Fix trimming warnings\"** or **\"fix IL warnings\"**\n- **\"Resolve IL2070 / IL2067 / IL2072 / IL2026 / IL3050 warnings\"**\n- **\"Add DynamicallyAccessedMembers annotations\"**\n- **\"Enable IsAotCompatible in my .csproj\"**\n- **\"My project has trim analyzer warnings after upgrading to net8.0\"**\n- **\"Annotate reflection code for the trimmer\"**\n\n## When Not to Use This Skill\n\nDo not use this skill when the project exclusively targets .NET Framework (net4x), which does not support the trim/AOT analyzers.\n\n## Prerequisites\n\nAn existing .NET project targeting net8.0 or later (or multi-targeting with at least one net8.0+ TFM) and the corresponding .NET SDK installed.\n\n## Background: What AOT Compatibility Means\n\nNative AOT and the IL trimmer perform static analysis to determine what code is reachable. Reflection can break this analysis because the trimmer can't see what types/members are accessed at runtime. The `IsAotCompatible` property enables analyzers that flag these issues as build warnings (ILXXXX codes).\n\n## Critical Rules\n\n### ❌ Never suppress warnings incorrectly\n\n- **NEVER** use `#pragma warning disable` for IL warnings. It hides warnings from the Roslyn analyzer at build time, but the IL linker and AOT compiler still see the issue. The code will fail at trim/publish time.\n- **NEVER** use `[UnconditionalSuppressMessage]`. It tells both the analyzer AND the linker to ignore the warning, meaning the trimmer cannot verify safety. Raising an error at build time is always preferable to hiding the issue and having it silently break at runtime.\n\n### 💡 Preferred approaches\n\n- **Prefer** `[DynamicallyAccessedMembers]` annotations to flow type information through the call chain.\n- **Prefer** refactoring to eliminate patterns that break annotation flow (e.g., boxing `Type` through `object[]`).\n- **Use** `[RequiresUnreferencedCode]` / `[RequiresDynamicCode]` / `[RequiresAssemblyFiles]` to mark methods as fundamentally incompatible with trimming, propagating the requirement to callers. This surfaces the issue clearly rather than hiding it — callers must explicitly acknowledge the incompatibility.\n\n### Annotation flow is key\n\nThe trimmer tracks `[DynamicallyAccessedMembers]` annotations through assignments, parameter passing, and return values. If this flow is broken (e.g., by boxing a `Type` into `object`, storing in an untyped collection, or casting through interfaces), the trimmer loses track and warns. The fix is to preserve the flow, not suppress the warning.\n\n## Step-by-Step Procedure\n\n> **Do not explore the codebase up-front.** The build warnings tell you exactly which files and lines need changes. Follow a tight loop: **build → pick a warning → open that file at that line → apply the fix recipe → rebuild**. Reading or analyzing source files beyond what a specific warning points you to is wasted effort and leads to timeouts. Let the compiler guide you.\n>\n> ❌ Do NOT run `find`, `ls`, or `grep` to understand the project structure before building. Do NOT read README, docs, or architecture files. Your first action should be Step 1 (enable AOT analysis), then build.\n\n### Step 1: Enable AOT analysis in the .csproj\n\nAdd `IsAotCompatible`. If the project doesn't exclusively target net8.0+, add a TFM condition (AOT analysis requires net8.0+):\n\n```xml\n<PropertyGroup>\n  <IsAotCompatible Condition=\"$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))\">true</IsAotCompatible>\n</PropertyGroup>\n```\n\nThis automatically sets `EnableTrimAnalyzer=true` and `EnableAotAnalyzer=true` for compatible TFMs. For multi-targeting projects (e.g., `netstandard2.0;net8.0`), the condition ensures no `NETSDK1210` warnings on older TFMs.\n\n### Step 2: Build and collect warnings\n\n```bash\ndotnet build <project.csproj> -f <net8.0-or-later-tfm> --no-incremental 2>&1 | grep 'IL[0-9]\\{4\\}'\n```\n\nSort and deduplicate. Common warning codes:\n- **IL2070**: Reflection call on a `Type` parameter missing `[DynamicallyAccessedMembers]`\n- **IL2067**: Passing an unannotated `Type` to a method expecting `[DynamicallyAccessedMembers]`\n- **IL2072**: Return value or extracted value missing annotation (often from unboxing)\n- **IL2057**: `Type.GetType(string)` with a non-constant argument\n- **IL2026**: Calling a method marked `[RequiresUnreferencedCode]`\n- **IL2050**: P/invoke method with COM marshalling parameters\n- **IL2075**: Return value flows into reflection without annotation\n- **IL2091**: Generic argument missing `[DynamicallyAccessedMembers]` required by constraint\n- **IL3000**: `Assembly.Location` returns empty string in single-file/AOT apps\n- **IL3050**: Calling a method marked `[RequiresDynamicCode]`\n\n### Step 3: Triage warnings by code (do NOT read every file)\n\nGroup the warnings from Step 2 by warning code and count them. **Do not open individual files yet.** Identify the top 1-2 patterns by count — these drive your fix strategy:\n\n| Pattern | Typical fix |\n|---------|-------------|\n| Many IL2026 + IL3050 from `JsonSerializer` | **Go to Strategy C immediately** — create a `JsonSerializerContext`, then batch-update all call sites |\n| IL2070/IL2087 on `Type` parameters | Add `[DynamicallyAccessedMembers]` to the innermost method, then cascade outward |\n| IL2067 passing unannotated `Type` | Annotate the parameter at the source |\n\n**In most real projects, IL2026/IL3050 from JsonSerializer dominate.** Start with Strategy C unless the warning breakdown clearly shows otherwise. After the batch JSON fix, handle remaining warnings with Strategies A–B. Only use Strategy D as a last resort.\n\n### Step 4: Fix warnings iteratively (innermost first)\n\nWork from the **innermost** reflection call outward. Each fix may cascade new warnings to callers.\n\n**Stay warning-driven.** For each warning, open only the file and line the compiler reported, identify the pattern, apply the matching fix recipe below, and move on. Do not scan the codebase for similar patterns or try to understand the full architecture — fix what the compiler tells you, rebuild, and let new warnings guide the next change. Fix a small batch of warnings (5-10), then rebuild immediately to check progress.\n\n**Use sub-agents when available.** If you can launch sub-agents (e.g., via a `task` tool), dispatch **multiple sub-agents in parallel** to edit different files simultaneously. Keep the main loop focused on building, parsing warnings, and dispatching — delegate actual file edits to sub-agents. For batch JSON updates, give each sub-agent 5-10 files to update in one prompt. **After 2 build-fix cycles, dispatch all remaining file edits to sub-agents in parallel — do not continue fixing files sequentially.** Example:\n\n> Update these files to use source-generated JSON: `src/Models/Resource.Serialization.cs`, `src/Models/Identity.Serialization.cs`, `src/Models/Plan.Serialization.cs`. In each file, replace `JsonSerializer.Serialize(writer, value)` with `JsonSerializer.Serialize(writer, value, MyProjectJsonContext.Default.TypeName)` and `JsonSerializer.Deserialize<T>(ref reader)` with `JsonSerializer.Deserialize(ref reader, MyProjectJsonContext.Default.TypeName)`. Only edit the JsonSerializer call sites.\n\n#### Strategy A: Add `[DynamicallyAccessedMembers]` (preferred)\n\nWhen a method uses reflection on a `Type` parameter, annotate the parameter to tell the trimmer what members are needed:\n\n```csharp\nusing System.Diagnostics.CodeAnalysis;\n\n// Before (warns IL2070):\nvoid Process(Type t) {\n    var method = t.GetMethod(\"Foo\");  // trimmer can't verify\n}\n\n// After (clean):\nvoid Process([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type t) {\n    var method = t.GetMethod(\"Foo\");  // trimmer preserves public methods\n}\n```\n\nWhen you annotate a parameter, **all callers** must now pass properly annotated types. This cascades outward — follow each caller and annotate or refactor as needed. **The caller's annotation must include at least the same member types as the callee's.** If the callee requires `PublicConstructors | NonPublicConstructors`, the caller must specify the same or a superset — using only `NonPublicConstructors` will produce IL2091.\n\n#### Strategy B: Refactor to preserve annotation flow\n\nWhen annotation flow is broken by boxing (storing `Type` in `object`, `object[]`, or untyped collections), **refactor** to pass the `Type` directly:\n\n```csharp\n// BROKEN: Type boxed into object[], annotation lost\nvoid Process(object[] args) {\n    Type t = (Type)args[0];  // IL2072: annotation lost through boxing\n    Evaluate(t, ...);\n}\n\n// FIXED: Pass Type as a separate, annotated parameter\nvoid Process(\n    object[] args,\n    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type calleeType,\n    ...) {\n    Evaluate(calleeType, ...);  // annotation flows cleanly\n}\n```\n\nCommon patterns that break flow and how to fix them:\n- **`object[]` parameter bags**: Extract the `Type` into a dedicated annotated parameter\n- **Dictionary/List storage**: Use a typed field with annotation instead\n- **Interface indirection**: Add annotation to the interface method's parameter\n- **Property with boxing getter**: Annotate the property's return type\n\n#### Strategy C: Source-generated JSON serialization (batch fix)\n\nWhen most warnings are IL2026/IL3050 from `JsonSerializer.Serialize`/`Deserialize`, this is a single mechanical fix applied in bulk:\n\n1. **Collect affected types** — grep for all `JsonSerializer.Serialize` and `JsonSerializer.Deserialize` call sites. Extract the type being serialized (the `<T>` in `Deserialize<T>`, or the runtime type of the object in `Serialize`).\n\n2. **Create one `JsonSerializerContext`** with `[JsonSerializable]` for every type found. **Skip types from external packages** (e.g., `ResponseError` from `Azure.Core`) — they won't source-generate for types you don't own. Handle external types separately via Gotcha #1 below.\n\n```csharp\n[JsonSerializerContext]\n[JsonSerializable(typeof(ManagedServiceIdentity))]\n[JsonSerializable(typeof(SystemData))]\n// ... one attribute per type YOU OWN\n// Do NOT add types from external packages (e.g., ResponseError)\ninternal partial class MyProjectJsonContext : JsonSerializerContext { }\n```\n\n3. **Batch-update all call sites** — do not read each file individually. Apply the pattern mechanically:\n   - `JsonSerializer.Serialize(obj)` → `JsonSerializer.Serialize(obj, MyProjectJsonContext.Default.TypeName)`\n   - `JsonSerializer.Deserialize<T>(json)` → `JsonSerializer.Deserialize(json, MyProjectJsonContext.Default.TypeName)`\n\n   Find and update all call sites in one pass:\n   ```bash\n   # Find all files with JsonSerializer calls\n   grep -rl 'JsonSerializer\\.\\(Serialize\\|Deserialize\\)' src/ --include='*.cs'\n   ```\n   Then use sequential `edit` calls to apply the same transformation to every matching file. **Do not use `sed` for C# code** — generics like `Deserialize<T>()` have angle brackets and nested parentheses that sed will mangle.\n\n4. **Build once** to verify. Remaining warnings will be non-serialization issues — handle those with Strategies A–B or D.\n\n#### Strategy D: `[RequiresUnreferencedCode]` (last resort)\n\nWhen a method fundamentally requires arbitrary reflection that cannot be statically described:\n\n```csharp\n[RequiresUnreferencedCode(\"Loads plugins by name using Assembly.Load\")]\npublic void LoadPlugin(string assemblyName) {\n    var asm = Assembly.Load(assemblyName);\n    // ...\n}\n```\n\nThis propagates to callers — they must also be annotated with `[RequiresUnreferencedCode]`. Use sparingly; it marks the entire call chain as trim-incompatible.\n\n### Step 5: Rebuild and repeat\n\nAfter each small batch of fixes (5-10 warnings), rebuild with `--no-incremental` and check for new warnings. **Do not attempt to fix all warnings before rebuilding** — frequent rebuilds catch mistakes early and reveal cascading warnings. Fixes cascade — annotating an inner method may surface warnings in its callers. Repeat until `0 Warning(s)`.\n\n### Step 6: Validate all TFMs\n\nBuild all target frameworks to ensure:\n- **0 IL warnings** on net8.0+ TFMs\n- **No NETSDK1210 warnings** (the `IsAotCompatible` condition handles this)\n- **Clean builds** on older TFMs (netstandard2.0, net472, etc.)\n\n```bash\ndotnet build <project.csproj>  # builds all TFMs\n```\n\n## Stop Signals\n\n- **Do not analyze more than 2-3 representative files per warning pattern.** After identifying the fix for a pattern, apply it to all matching files without reading each one first.\n- **Start fixing after one build.** Do not do a second analysis pass — begin implementing fixes for the most common warning pattern immediately after Step 3 triage.\n- Stop after achieving **0 IL warnings** for net8.0+ TFMs. Don't optimize or refactor already-clean annotations.\n- If a warning requires **architectural refactoring** beyond annotation flow fixes (e.g., replacing an entire serialization layer), document it and stop — don't rewrite large subsystems.\n- Limit to **3 build-fix iterations** per warning. If annotation flow doesn't resolve it after 3 attempts, escalate to `[RequiresUnreferencedCode]`.\n- Don't chase warnings in **third-party dependencies** you can't modify. Note them and move on.\n- If the user asked a scoped question (e.g., \"fix warnings in this file\"), don't expand to the entire project.\n\n## Polyfills for Older TFMs\n\nFor multi-targeting projects that include netstandard2.0 or net472, you need polyfills for `DynamicallyAccessedMembersAttribute` and related types. See [references/polyfills.md](references/polyfills.md).\n\n## Common Gotchas\n\n1. **External types without AOT-safe serialization**: When a type comes from a dependency you can't modify (e.g., `ResponseError` from `Azure.Core`) and it lacks a source-generated serializer, `Options.GetConverter<T>()` is reflection-based and will produce IL warnings. First check if the type implements `IJsonModel<T>` (common in Azure SDK) — if so, bypass `JsonSerializer` entirely:\n\n```csharp\n// Before (IL2026 — JsonSerializer uses reflection):\nJsonSerializer.Serialize(writer, errorValue);\n\n// After (AOT-safe — uses IJsonModel directly):\n((IJsonModel<ResponseError>)errorValue).Write(writer, ModelReaderWriterOptions.Json);\n\n// For deserialization:\nvar error = ((IJsonModel<ResponseError>)new ResponseError()).Create(ref reader, ModelReaderWriterOptions.Json);\n```\n\nDo **not** add the external type to your `JsonSerializerContext` — it won't source-generate for types you don't own. If the type doesn't implement `IJsonModel<T>`, write a custom `JsonConverter<T>` with manual `Utf8JsonReader`/`Utf8JsonWriter` logic and register it via `[JsonSourceGenerationOptions]` on your context.\n\n2. **Serialization libraries**: Most reflection-based serializers (e.g., `Newtonsoft.Json`, `XmlSerializer`) are not AOT-compatible. Migrate to a source-generation-based serializer such as `System.Text.Json` with a `JsonSerializerContext`. If migration is not feasible, mark the serialization call site with `[RequiresUnreferencedCode]`.\n\n3. **Shared projects / projitems**: When source is shared between multiple projects via `<Import>`, annotations added to shared code affect ALL consuming projects. Verify that all consumers still build cleanly.\n\n## References\n\n[Limitations](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=windows%2Cnet8#limitations-of-native-aot-deployment)\n[Conceptual: Understanding trimming](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-concepts)\n[How-to: trim compat](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/fixing-warnings)\n\n## Checklist\n\n- [ ] Added `<IsAotCompatible>` with TFM condition to .csproj\n- [ ] Built with AOT analyzers enabled (net8.0+ TFM)\n- [ ] Fixed all IL warnings via annotations or refactoring\n- [ ] No `#pragma warning disable` or `[UnconditionalSuppressMessage]` used for any IL warning\n- [ ] Polyfills present for older TFMs if needed\n- [ ] All target frameworks build with 0 warnings\n- [ ] Verified shared/linked source doesn't break sibling projects\n","frontmatter":{"name":"dotnet-aot-compat","description":"Make .NET projects compatible with Native AOT and trimming by systematically resolving IL trim/AOT analyzer warnings. USE FOR: making projects AOT-compatible, fixing trimming warnings, resolving IL warnings (IL2026, IL2070, IL2067, IL2072, IL3050), adding DynamicallyAccessedMembers annotations, enabling IsAotCompatible. DO NOT USE FOR: publishing native AOT binaries, optimizing binary size, replacing reflection-heavy libraries with alternatives. INVOKES: no tools — pure knowledge skill.\n","license":"MIT"},"isInternal":false,"tokens":3905,"sizeBytes":16833},{"name":"SKILL.md","path":".github/skills/integrating-sdk-and-msbuild/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/integrating-sdk-and-msbuild/SKILL.md","title":"integrating-sdk-and-msbuild","category":"anthropic-skill","format":"markdown","content":"---\nname: integrating-sdk-and-msbuild\ndescription: 'Guides work on the SDK-MSBuild integration boundary. Consult when authoring or modifying SDK targets, working on dotnet CLI to MSBuild invocation, handling project-reference protocol, coordinating cross-repo changes with dotnet/sdk, debugging property resolution or import ordering, working on restore/build/publish/pack target chains, or dealing with Directory.Build.props/targets interaction.'\nargument-hint: 'Describe the SDK integration scenario or cross-repo coordination need.'\n---\n\n# SDK-MSBuild Integration Patterns\n\nMSBuild operates as a component within the .NET SDK. This boundary is the most complex integration point in the .NET build stack, spanning MSBuild (engine), SDK (target implementations), NuGet (restore), and Roslyn (compilation).\n\n## The Evaluation Boundary\n\nUnderstanding MSBuild's evaluation order is critical for SDK target authoring:\n\n```\n1. Environment variables\n2. Global properties (from CLI: -p:Foo=Bar)\n3. Project-level properties (file order, with imports):\n   ┌─ Sdk.props (SDK defaults)\n   ├─ Directory.Build.props (user overrides BEFORE project)\n   ├─ <Project> properties (the .csproj itself)\n   ├─ Directory.Build.targets (user overrides AFTER project)\n   └─ Sdk.targets (SDK target definitions)\n4. Item definitions\n5. Items (including SDK default globs)\n```\n\n### Key Import Order Rules\n\n- **SDK props import BEFORE user project** — SDK defaults can be overridden by the user\n- **SDK targets import AFTER user project** — SDK targets see user-specified properties\n- **`Directory.Build.props` is imported from `Microsoft.Common.props` as an early user extension point after core defaults are computed** — use it for solution-wide customization\n- **Property defaults set in SDK must not override user-specified values** — always use `Condition=\"'$(Prop)' == ''\"`\n\n```xml\n<!-- CORRECT: SDK default that respects user override -->\n<OutputType Condition=\"'$(OutputType)' == ''\">Library</OutputType>\n\n<!-- WRONG: Unconditional set clobbers user's .csproj -->\n<OutputType>Library</OutputType>\n```\n\n## Restore and Build Separation\n\n**Restore and Build must never run in the same evaluation.** The restore phase generates `.g.props` and `.g.targets` files that must be imported during evaluation — but they don't exist until restore completes.\n\n- `dotnet build` implicitly runs restore then build as **separate invocations**\n- `dotnet build --no-restore` skips restore, assuming it already happened\n- Running both targets in one invocation (`/t:Restore;Build`) is a known anti-pattern that causes intermittent failures\n\n## Project Reference Protocol\n\nThe project-reference protocol spans MSBuild, SDK, and NuGet. It is the most complex integration boundary.\n\n### How It Works\n\n1. Outer build dispatches to `_GetProjectReferenceTargetFrameworkProperties` to determine inner build parameters\n2. Inner build runs with the resolved `TargetFramework` (singular) for each referenced project\n3. `GetTargetPath` returns the output assembly for the referencing project to consume\n\n### Rules\n\n- Protocol changes must be coordinated across MSBuild, SDK, and NuGet teams\n- Multi-targeting projects (`<TargetFrameworks>`) dispatch multiple inner builds\n- The outer build must not assume a single target framework\n- `SetTargetFramework` is how the outer build communicates the chosen framework to inner builds\n\n## Target Authoring in SDK Context\n\n### Extension Points\n\nSDK provides well-known extension points for targets:\n\n| Extension Point | Use For |\n|----------------|---------|\n| `$(BuildDependsOn)` | Adding to the Build chain |\n| `$(CompileDependsOn)` | Pre-compilation steps |\n| `$(PublishDependsOn)` | Publish pipeline additions |\n| `$(PackDependsOn)` | NuGet pack pipeline additions |\n| `BeforeTargets=\"Build\"` | Use sparingly; prefer `DependsOnTargets` |\n\n### Ordering Rules\n\n1. **Use `DependsOnTargets` for required predecessors** — it's explicit and predictable\n2. **`BeforeTargets`/`AfterTargets` should be used sparingly** — they create implicit ordering that's hard to debug\n3. **Incremental build targets need precise `Inputs` and `Outputs`** — incorrect declarations cause either rebuild-every-time or stale-output bugs\n4. **Test with multi-targeting** — target chains execute once per `TargetFramework` in the inner build\n\n## Cross-Repo Coordination\n\nChanges that touch the MSBuild-SDK boundary often require coordinated PRs:\n\n1. **MSBuild engine change** → may need SDK target updates\n2. **SDK target change** → may need MSBuild API additions\n3. **NuGet restore change** → affects both MSBuild evaluation and SDK targets\n\n### Coordination Protocol\n\n- File an issue in both repos describing the cross-cutting change\n- Land the MSBuild change first (lower in the stack)\n- Update SDK to consume the new MSBuild via dependency flow\n- Test end-to-end with the SDK's MSBuild integration tests\n\n## Design-Time Builds\n\nVisual Studio uses design-time builds with different target contracts:\n\n- Design-time builds call `ResolveProjectReferences` but not `Build`\n- They set `$(DesignTimeBuild)=true` and `$(BuildingProject)=false`\n- Targets that should not run during design-time must check these properties\n- Design-time builds must be fast — avoid expensive I/O or compilation\n\n## Common Integration Bugs\n\n| Symptom | Likely Cause |\n|---------|-------------|\n| Property has wrong value | Import ordering — check if SDK prop overrides user setting |\n| Target runs in wrong order | Missing `DependsOnTargets` declaration |\n| Build works, restore fails | Evaluation-time dependency on restore-generated files |\n| Works single-target, fails multi-target | Target assumes single `$(TargetFramework)` |\n| CLI build works, VS build fails | Design-time build target contract violation |\n","frontmatter":{"name":"integrating-sdk-and-msbuild","description":"Guides work on the SDK-MSBuild integration boundary. Consult when authoring or modifying SDK targets, working on dotnet CLI to MSBuild invocation, handling project-reference protocol, coordinating cross-repo changes with dotnet/sdk, debugging property resolution or import ordering, working on restore/build/publish/pack target chains, or dealing with Directory.Build.props/targets interaction.","argument-hint":"Describe the SDK integration scenario or cross-repo coordination need."},"isInternal":false,"tokens":1250,"sizeBytes":5807},{"name":"SKILL.md","path":".github/skills/maintaining-binary-log-compatibility/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/maintaining-binary-log-compatibility/SKILL.md","title":"maintaining-binary-log-compatibility","category":"anthropic-skill","format":"markdown","content":"---\nname: maintaining-binary-log-compatibility\ndescription: 'Guides changes to MSBuild binary log infrastructure. Consult when modifying BinaryLogger or BinaryLogReplayEventSource, adding new BuildEventArgs types, changing event serialization/deserialization, modifying ProjectImportsCollector, adjusting message importance levels, or making changes that affect .binlog content. Also applies when verifying that behavioral changes are properly reflected in binary log output.'\nargument-hint: 'Describe the binary log change or event serialization concern.'\n---\n\n# Binary Log Considerations\n\nThe binary log (`.binlog`) is MSBuild's primary diagnostic format and source of truth for build analysis. It captures the complete build event stream with full fidelity. Format changes have strict compatibility requirements.\n\nFor general binary log usage, see [Binary-Log.md](../../../documentation/wiki/Binary-Log.md).\n\n## Core Principles\n\n1. **The binlog captures everything.** All meaningful build events must be represented. If your change alters build behavior, it must be observable in the binlog.\n2. **Format changes must be backward-compatible.** Older versions of MSBuild Structured Log Viewer and other tools must be able to read binlogs produced by newer MSBuild, at minimum gracefully degrading.\n3. **Forward compatibility matters too.** Newer viewers should handle binlogs from older MSBuild without crashing, even if some events are unrecognized.\n\n## Architecture Overview\n\n```\nBuild Engine\n  → BuildEventArgs (structured events)\n    → BinaryLogger (serializes to .binlog)\n      → ProjectImportsCollector (embeds project/targets files)\n\nReplay:\n  .binlog file\n    → BinaryLogReplayEventSource (deserializes)\n      → Any ILogger (console, structured log viewer, analyzers)\n```\n\nKey source files:\n- `src/Build/Logging/BinaryLogger/BinaryLogger.cs` — the logger itself\n- `src/Framework/BuildEventArgs.cs` — base class for all build events\n- `src/Build/Logging/BinaryLogger/ProjectImportsCollector.cs` — captures imported files\n\n## Adding New Build Event Types\n\nWhen adding a new `BuildEventArgs` subclass:\n\n1. **Define the new event class** inheriting from the appropriate base (`BuildMessageEventArgs`, `BuildWarningEventArgs`, etc.)\n2. **Add serialization and forwarding support** — implement `WriteToStream` and `CreateFromStream`, then trace the event through every dispatcher and transport (including task-host forwarding) without making unknown event types fatal\n3. **Increment the binary log version** if the new event type changes the format\n4. **Add a new record type constant** in the binary logger's record type enum\n5. **Handle the unknown-type case** in the replay source — older readers must skip gracefully\n\n### Serialization Compatibility Rules\n\n- **Never remove fields** from existing event args serialization\n- **New fields must be appended** to the end of the serialization stream\n- **Use version checks** when reading — if the binlog version is older, use defaults for new fields\n- **Nullable fields** should serialize a presence flag before the value\n\n```csharp\n// Pattern for backward-compatible field addition\nif (logVersion >= newFieldVersion)\n{\n    writer.Write(newField);\n}\n\n// Reading with backward compatibility\nif (logVersion >= newFieldVersion)\n{\n    newField = reader.ReadString();\n}\nelse\n{\n    newField = defaultValue;\n}\n```\n\n## Message Importance Levels\n\nImportance controls what appears in console output, but **everything goes to binlog** regardless of importance.\n\n| Level | Use For | Console Verbosity |\n|-------|---------|------------------|\n| `High` | Critical user-facing information | Minimal and above |\n| `Normal` | Standard build progress | Normal and above |\n| `Low` | Detailed diagnostic information | Detailed and above |\n| `Diagnostic` | Internal debugging | Diagnostic only |\n\n### Rules\n\n- Default to `Normal` for user-relevant information\n- Use `Low` for information useful when debugging but noisy in normal builds\n- `High` is reserved for important warnings/status — use sparingly\n- Never skip logging because \"it's too verbose\" — log at `Low` instead\n\n## ProjectImportsCollector\n\nThe `ProjectImportsCollector` embeds all imported `.props`, `.targets`, and project files into the binlog. This enables the \"preprocessed view\" in log viewers.\n\n### Considerations\n\n- `MSBUILDLOGIMPORTS=1` (or the `/bl` switch) enables import collection\n- Imported file content is captured at evaluation time — reflects the actual content used\n- Large import chains increase binlog size — this is acceptable for diagnostic completeness\n- Sensitive content in imported files will be embedded — document this for users\n\n## Changes That Affect Binlog Content\n\nWhen modifying MSBuild behavior, verify binlog impact:\n\n| Change Type | Binlog Consideration |\n|------------|---------------------|\n| New property set during evaluation | Appears in `PropertyInitialValue` or `PropertyReassignment` events |\n| New target added | Produces `TargetStarted`/`TargetFinished` events |\n| Changed task behavior | Task output items/properties captured in `TaskFinished` |\n| New warning/error | Captured as `BuildWarningEventArgs`/`BuildErrorEventArgs` |\n| Modified import chain | Changes which files `ProjectImportsCollector` captures |\n\n## Testing Binlog Changes\n\n- **Round-trip test**: Write a binlog, replay it, verify all events are reconstructed\n- **Version compatibility test**: Verify older replay sources handle new events gracefully\n- **Content verification**: Assert specific events appear in the binlog for behavioral changes\n- **Size regression**: Monitor binlog size for unexpectedly large increases\n- Use `BinaryLogReplayEventSource` in tests to verify binlog content programmatically\n\n## Checklist\n\n- [ ] New event types have `WriteToStream`/`CreateFromStream` implementations\n- [ ] Binary log format version incremented if format changed\n- [ ] Backward compatibility: older readers skip/degrade gracefully\n- [ ] Forward compatibility: newer readers handle old format\n- [ ] Importance levels set correctly for new messages\n- [ ] Behavioral changes produce observable binlog events\n- [ ] Round-trip test passes (serialize → deserialize → verify)\n","frontmatter":{"name":"maintaining-binary-log-compatibility","description":"Guides changes to MSBuild binary log infrastructure. Consult when modifying BinaryLogger or BinaryLogReplayEventSource, adding new BuildEventArgs types, changing event serialization/deserialization, modifying ProjectImportsCollector, adjusting message importance levels, or making changes that affect .binlog content. Also applies when verifying that behavioral changes are properly reflected in binary log output.","argument-hint":"Describe the binary log change or event serialization concern."},"isInternal":false,"tokens":1293,"sizeBytes":6211},{"name":"SKILL.md","path":".github/skills/merge-dependency-updates/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/merge-dependency-updates/SKILL.md","title":"merge-dependency-updates","category":"anthropic-skill","format":"markdown","content":"---\nname: merge-dependency-updates\ndescription: 'Review and merge open bot PRs: dependency updates from dotnet-maestro, codeflow from dotnet/dotnet, and OneLoc localization PRs. Produces a clickable dashboard with CI status, review state, and suspicious file flags. Use when you want to triage all open bot PRs in one pass.'\nargument-hint: 'Triage, update, and version-bump open bot PRs.'\n---\n\n# Managing Bot PRs (Dependencies, Codeflow, OneLoc)\n\nThis skill triages all open bot PRs in the MSBuild repo, produces a review dashboard, then optionally updates branches and bumps versions for merge readiness.\n\n## When to Use\n\n- You want to see the status of all open bot PRs at a glance\n- There are open dependency update / codeflow / OneLoc PRs that need merging\n- You want to process all of them in one pass with a clickable summary\n\n## PR Categories\n\n| Category | Author | Title Pattern | Example |\n|----------|--------|---------------|---------|\n| Dependency update | `dotnet-maestro[bot]` | `[<branch>] Update dependencies from dotnet/<repo>` | `[vs18.3] Update dependencies from dotnet/arcade` |\n| Codeflow | `dotnet-maestro[bot]` | `[<branch>] Source code updates from dotnet/dotnet` | `[vs18.3] Source code updates from dotnet/dotnet` |\n| OneLoc | `dotnet-bot` | `Localized file check-in by OneLocBuild Task: ...` | `Localized file check-in by OneLocBuild Task: Build definition ID 9434: Build ID ...` |\n\n## Step-by-Step Procedure\n\n### Step 1: Gather All Bot PRs\n\nRun **two searches in parallel** to find all open bot PRs:\n\n1. **Dependency updates + Codeflow:**\n   ```\n   Search: is:open author:app/dotnet-maestro repo:dotnet/msbuild\n   ```\n\n2. **OneLoc localization:**\n   ```\n   Search: is:open author:dotnet-bot repo:dotnet/msbuild (\"OneLocBuild\" OR \"Localized file check-in\")\n   ```\n\n### Step 2: Analyze Each PR (in parallel)\n\nFor each PR, query the following data using `gh api graphql`. Process PRs in parallel batches for speed.\n\n```bash\ngh api graphql -f query='\n  query {\n    repository(owner: \"dotnet\", name: \"msbuild\") {\n      pullRequest(number: <PR_NUMBER>) {\n        id\n        title\n        baseRefName\n        headRefName\n        mergeable\n        reviewDecision\n        commits(last: 1) {\n          nodes {\n            commit {\n              statusCheckRollup {\n                state\n              }\n            }\n          }\n        }\n        reviews(last: 10) {\n          nodes {\n            state\n            author { login }\n          }\n        }\n        reviewThreads(first: 20) {\n          nodes {\n            isResolved\n            comments(first: 1) {\n              nodes { body author { login } }\n            }\n          }\n        }\n        files(first: 50) {\n          nodes { path additions deletions }\n          totalCount\n        }\n      }\n    }\n  }\n'\n```\n\nFrom the response, extract:\n- **CI status**: from `statusCheckRollup.state` — `SUCCESS`, `FAILURE`, `PENDING`, `ERROR`, `EXPECTED`, or `null`. Map `ERROR` to failure (❌) and `EXPECTED` to neutral (⚪) in the dashboard.\n- **Review decision**: `APPROVED`, `REVIEW_REQUIRED`, `CHANGES_REQUESTED`\n- **Approvals**: count and list of `APPROVED` reviews\n- **Review comments**: any unresolved threads from the first 20 results (especially `CHANGES_REQUESTED`)\n- **Files changed**: use `totalCount` for the file count; use the first 50 file paths from `files.nodes` for suspicious file detection\n- **Suspicious flag**: check if any file paths are outside expected patterns (see below)\n\n#### Suspicious File Detection\n\n| PR Category | Expected Files | Flag if... |\n|-------------|---------------|------------|\n| Dependency update (arcade) | `eng/**`, `global.json` | Any file outside `eng/` or `global.json` |\n| Dependency update (roslyn/nuget/runtime) | `eng/Version.Details.xml`, `eng/Version.Details.props` | Any file outside `eng/` |\n| Codeflow (dotnet/dotnet) | `eng/Version.Details.xml` | Any `src/` files modified |\n| OneLoc | `**/xlf/*.xlf`, `**/Resources/*.resx` | Any non-localization file, or reviewers flagging reverted translations |\n\n### Step 3: Update Branches & Bump Versions (dependency PRs only)\n\nFor each **dependency update** PR:\n\n#### 3a. Update the PR branch\n\nUse the GitHub GraphQL `updatePullRequestBranch` mutation — the exact equivalent of the \"Update branch\" button:\n\n```bash\ngh api graphql -f query='\n  mutation {\n    updatePullRequestBranch(input: {pullRequestId: \"<NODE_ID>\"}) {\n      pullRequest { number headRefOid }\n    }\n  }\n'\n```\n\n#### 3b. Bump VersionPrefix (vs < 18.10 branches only)\n\n**Skip for `vs18.10` and newer vs* branches** - they don't need to increment the patch version with every dependency update anymore. Only bump for `vs*` branches that are older than `vs18.10`.\n\n1. Read `<VersionPrefix>X.Y.Z</VersionPrefix>` from the **target branch's** `eng/Versions.props`\n2. Increment patch: `X.Y.Z` → `X.Y.(Z+1)`\n3. Checkout the PR branch, update the version, commit, push:\n   ```bash\n   git fetch origin <head-branch>\n   git checkout <head-branch>\n   # Edit eng/Versions.props: set <VersionPrefix> to X.Y.(Z+1)\n   git add eng/Versions.props\n   git commit -m \"Bump VersionPrefix to X.Y.(Z+1)\"\n   git push origin <head-branch>\n   ```\n\n**Note:** The `<VersionPrefix>` line may be inline with `<DotNetFinalVersionKind>` — preserve that formatting.\n\n### Step 4: Produce the Dashboard\n\nOutput a **single dashboard** with all PRs grouped by category. This is the most important output — it lets the user click through and approve/reject each PR.\n\nFormat:\n\n```\n## 🚦 Dependency Update PRs\n\n| PR | Branch | CI | Review | Files | Suspicious? | Link |\n|----|--------|-----|--------|-------|-------------|------|\n| #13281 | main ← roslyn | ✅ SUCCESS | ✅ APPROVED (1) | 2 | ✅ Clean | https://github.com/dotnet/msbuild/pull/13281 |\n| #13310 | vs17.10 ← arcade | ❌ FAILURE | 🔶 REVIEW_REQUIRED | 14 | ✅ Clean (eng/ only) | https://github.com/dotnet/msbuild/pull/13310 |\n\n## 🔄 Codeflow PRs\n\n| PR | Branch | CI | Review | Files | Suspicious? | Link |\n|----|--------|-----|--------|-------|-------------|------|\n| #13258 | vs18.3 ← dotnet/dotnet | ✅ SUCCESS | 🔶 REVIEW_REQUIRED | 1 | ✅ Clean | https://github.com/dotnet/msbuild/pull/13258 |\n\n## 🌐 OneLoc PRs\n\n| PR | Branch | CI | Review | Files | Suspicious? | Link |\n|----|--------|-----|--------|-------|-------------|------|\n| #13290 | main | ⚪ None | ⛔ CHANGES_REQUESTED | 65 | ⚠️ Reverts translations | https://github.com/dotnet/msbuild/pull/13290 |\n```\n\nStatus icons:\n- CI: ✅ SUCCESS, ❌ FAILURE/ERROR, ⏳ PENDING, ⚪ None/EXPECTED\n- Review: ✅ APPROVED, 🔶 REVIEW_REQUIRED, ⛔ CHANGES_REQUESTED\n- Suspicious: ✅ Clean, ⚠️ with description\n\nAfter the tables, add a summary:\n> **N PRs total. M ready to merge, K need reviews, J have CI failures, L blocked.**\n\n## Version File Reference\n\nThe version is defined in `eng/Versions.props`:\n\n```xml\n<PropertyGroup>\n  <VersionPrefix>18.3.3</VersionPrefix>\n</PropertyGroup>\n```\n\n| Branch Pattern | VersionPrefix Format | Bump Needed? | Pre-release Label |\n|---------------|---------------------|-------------|------------------|\n| `main`        | `Major.Minor.0`     | No          | `preview`        |\n| `vs*`         | `Major.Minor.Patch` | Yes (+1)    | `servicing` or `preview` |\n\n## Troubleshooting\n\n| Problem | Solution |\n|---------|----------|\n| `updatePullRequestBranch` fails | PR may have conflicts GitHub can't auto-resolve — fall back to local `git merge` |\n| Push rejected after version bump | Branch was updated by maestro; re-fetch and retry |\n| VersionPrefix has no patch component (e.g. `18.6.0` on main) | This is main — skip the bump |\n| OneLoc PR flagged as \"reverting translations\" | Do NOT merge — wait for updated translations |\n","frontmatter":{"name":"merge-dependency-updates","description":"Review and merge open bot PRs: dependency updates from dotnet-maestro, codeflow from dotnet/dotnet, and OneLoc localization PRs. Produces a clickable dashboard with CI status, review state, and suspicious file flags. Use when you want to triage all open bot PRs in one pass.","argument-hint":"Triage, update, and version-bump open bot PRs."},"isInternal":false,"tokens":2052,"sizeBytes":7728},{"name":"SKILL.md","path":".github/skills/optimizing-msbuild-performance/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/optimizing-msbuild-performance/SKILL.md","title":"optimizing-msbuild-performance","category":"anthropic-skill","format":"markdown","content":"---\nname: optimizing-msbuild-performance\ndescription: 'Guides performance optimization for MSBuild engine code. Consult when working on hot paths in evaluation or execution, reducing allocations, choosing collection types, handling strings efficiently, modifying Expander.cs or Evaluator.cs, using Span-based APIs and stackalloc, caching values, or profiling build performance. Also applies when reviewing PRs for performance regression.'\nargument-hint: 'Describe the performance-sensitive code area or optimization goal.'\n---\n\n# MSBuild Performance Guidelines\n\nMSBuild evaluates and builds thousands of projects in enterprise solutions. Performance is an architectural concern, not an afterthought.\n\n## Guiding Principle\n\n**Profile before optimizing; measure, do not guess.** Use BenchmarkDotNet, the evaluation profiler (`/profileevaluation`), or ETW traces to identify actual bottlenecks before optimizing.\n\nSee [evaluation-profiling.md](../../../documentation/evaluation-profiling.md) and [General_perf_onepager.md](../../../documentation/specs/proposed/General_perf_onepager.md) for profiling tools.\n\n## Allocation Awareness on Hot Paths\n\nThe evaluation and execution engines process millions of operations per build. Unnecessary allocations cause GC pressure that compounds across large solutions.\n\n### Rules\n\n1. **Avoid LINQ in hot paths.** `Where`, `Select`, `Any`, `First` all allocate enumerator objects and delegate closures. Use `foreach` loops instead.\n\n   ```csharp\n   // BAD — allocates iterator + delegate\n   var match = items.FirstOrDefault(i => i.Name == name);\n\n   // GOOD — zero allocations\n   foreach (var item in items)\n   {\n       if (item.Name == name) { /* found */ break; }\n   }\n   ```\n\n2. **Avoid string allocations in formatting.** Use `string.Concat`, interpolation with `Span<T>`, or `StringBuilder` reuse — not `string.Format` on hot paths.\n\n3. **Prefer `Span<T>` and `stackalloc` for short-lived buffers** when parsing or slicing strings. Avoid `Substring` when you only need to compare or inspect a portion.\n\n4. **Cache computed values.** If a value is computed in a loop but doesn't change, hoist it out. If it's expensive and reused across calls, use a field or `Lazy<T>`.\n\n## String Comparison Rules\n\nMSBuild property, item, and target names are **case-insensitive**. Getting this wrong causes subtle bugs and perf issues.\n\n| Scenario | Use |\n|----------|-----|\n| Property/item/target names | `MSBuildNameIgnoreCaseComparer` |\n| General MSBuild identifiers | `StringComparison.OrdinalIgnoreCase` |\n| Dictionary keys for MSBuild names | `MSBuildNameIgnoreCaseComparer` as comparer |\n| File paths on Windows | `StringComparison.OrdinalIgnoreCase` |\n| File paths on Linux | `StringComparison.Ordinal` |\n\n**Never** use `ToLower()`/`ToUpper()` for comparisons — they allocate a new string every time.\n**Never** use `CurrentCulture` for MSBuild identifiers — build behavior must not vary by locale.\n\n## Collection Type Selection\n\n| Access Pattern | Recommended Type |\n|---------------|-----------------|\n| Small fixed set (< ~8 items) | Array or `ReadOnlySpan<T>` |\n| Build-once, read-many | `ImmutableArray<T>` (not `ImmutableList<T>`) |\n| Keyed lookup, many items | `Dictionary<TKey, TValue>` with appropriate comparer |\n| Keyed lookup, few items (< 5) | Linear scan over array (cache-friendly, avoids dict overhead) |\n| Concurrent reads, rare writes | `ImmutableDictionary` or snapshot pattern |\n| Ordered iteration needed | `List<T>` or array; avoid `HashSet<T>` if order matters |\n\n**`ImmutableList<T>` is almost never the right choice** — it has O(log n) access vs O(1) for `ImmutableArray<T>`.\n\n## Hot Path Identification\n\nThese areas are performance-critical and require extra scrutiny:\n\n- **`Expander.cs`** — property/item/metadata expansion during evaluation\n- **`Evaluator.cs`** — project evaluation orchestration\n- **`ItemSpec.cs` / `LazyItemEvaluator.cs`** — item evaluation and globbing\n- **`TaskExecutionHost.cs`** — task parameter marshaling\n- **File I/O paths** — `FileMatcher.cs`, glob operations, project loading\n\n### Inlining Considerations\n\nFor extremely hot methods, consider:\n- `[MethodImpl(MethodImplOptions.AggressiveInlining)]` for small methods called millions of times\n- Avoiding virtual dispatch in inner loops\n- Using `struct` enumerators to avoid heap allocation\n\n## Evaluation Performance Specifics\n\n- **Import chain depth** affects evaluation time linearly. Minimize unnecessary imports.\n- **Glob patterns** are evaluated per-project. Overly broad globs (`**/*`) are expensive.\n- **Condition evaluation** should use short-circuit logic — put cheap checks first.\n- **Property functions** (`$([System.IO.Path]::...)`) are interpreted and slower than built-in operations.\n\n## Anti-Patterns\n\n| Anti-Pattern | Why It's Bad | Fix |\n|-------------|-------------|-----|\n| `items.Count() > 0` | Enumerates entire collection | `items.Any()` or check `.Count` property |\n| `string.Format` in log messages at Low importance | Allocates even if message is filtered | Use structured logging or guard with verbosity check |\n| `new List<T>(enumerable).ToArray()` | Double allocation | `enumerable.ToArray()` directly |\n| `dict.ContainsKey(k) then dict[k]` | Double lookup | `dict.TryGetValue(k, out var v)` |\n| Regex in a loop without `RegexOptions.Compiled` | Reinterprets pattern each time | Compile or use static `Regex` field |\n","frontmatter":{"name":"optimizing-msbuild-performance","description":"Guides performance optimization for MSBuild engine code. Consult when working on hot paths in evaluation or execution, reducing allocations, choosing collection types, handling strings efficiently, modifying Expander.cs or Evaluator.cs, using Span-based APIs and stackalloc, caching values, or profiling build performance. Also applies when reviewing PRs for performance regression.","argument-hint":"Describe the performance-sensitive code area or optimization goal."},"isInternal":false,"tokens":1183,"sizeBytes":5403},{"name":"SKILL.md","path":".github/skills/pipelines-health-check/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/pipelines-health-check/SKILL.md","title":"pipelines-health-check","category":"anthropic-skill","format":"markdown","content":"---\nname: pipelines-health-check\ndescription: Check health of MSBuild CI pipelines, VS repo PR insertion statuses, and VMR codeflow PRs. Use when asked about pipeline health, build failures, infrastructure issues, CI status, insertion PR status, VMR codeflow status, or for periodic health monitoring.\n---\n\n# Pipelines & PR Health Check\n\nThis skill checks the health of MSBuild's CI pipelines, the status of insertion PRs in the VS repository, and the health of VMR codeflow PRs that flow MSBuild source into the dotnet/dotnet unified repository.\n\n## When to Use\n\n- User asks about MSBuild pipeline health, CI status, or build failures\n- User asks about VS insertion PR status or whether insertions are going through\n- User asks about VMR codeflow PRs, dotnet/dotnet PR status, or dotnet-unified-build pipeline\n- User asks to check if there are failing checks on PRs\n- User asks for a health check or status overview\n- Periodic monitoring requests\n\n## Prerequisites\n\n- `az` CLI must be installed and authenticated (`az login` with access to the DevDiv organization)\n- Azure DevOps extension for `az` must be installed: `az extension add --name azure-devops`\n- PowerShell 5.1+ or PowerShell Core\n\n### Optional: WorkIQ (for infrastructure issue investigation)\n\n[WorkIQ](https://www.npmjs.com/package/@microsoft/workiq) is an MCP server / CLI that can query Microsoft 365 data (people, emails, Teams, documents) to find **service ownership, contacts, and incident context** when pipeline failures are caused by infrastructure issues outside MSBuild's control.\n\n**Check availability:**\n```powershell\nworkiq version\n# Expected: 0.2.x or later\n```\n\n**If not installed**, set it up:\n```powershell\n# Install globally (use --registry if your .npmrc redirects @microsoft scope to GitHub Packages)\nnpm install -g @microsoft/workiq --registry https://registry.npmjs.org\n\n# Accept the EULA (required once)\nworkiq accept-eula\n```\n\nWorkIQ is not required for the core health check. If unavailable, the skill will still work — it will simply skip the ownership lookup and suggest manual investigation or offer to help install WorkIQ.\n\n## Reference Information\n\n### Pipelines\n\n| Pipeline | Org/Project | ID | Purpose |\n|----------|-------------|----|---------|\n| MSBuild | devdiv/DevDiv | 9434 | Main CI pipeline — builds and tests on every commit to main |\n| MSBuild-OptProf | devdiv/DevDiv | 17389 | Optimization/profiling pipeline — runs on schedule |\n| dotnet-unified-build | dnceng-public/public | — | VMR build pipeline — runs on codeflow PRs in dotnet/dotnet |\n\n### Key URLs\n\n- MSBuild pipeline: `https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434`\n- OptProf pipeline: `https://devdiv.visualstudio.com/DevDiv/_build?definitionId=17389`\n- VS PRs assigned to MSBuild: `https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequests?_a=active&assignedTo=66cc9d27-aef7-4399-ba2c-3dccb4489098`\n- VMR codeflow PRs: `https://github.com/dotnet/dotnet/pulls?q=is:pr+is:open+\"Source+code+updates+from+dotnet/msbuild\"`\n- dotnet-unified-build (public): `https://dev.azure.com/dnceng-public/public/_build`\n\n## Phase 1: Collect Data & Present Overview Table\n\n### Step 1: Run all data collection scripts\n\nRun these three scripts from the repository root **in parallel**. They output JSON to stdout. Each script may take 1–3 minutes depending on the number of PRs and pipeline runs to fetch, so use an `initial_wait` of **120 seconds** or higher.\n\n```powershell\n# Pipeline health (checks both MSBuild and MSBuild-OptProf)\n$pipelineJson = & .\\.github\\skills\\pipelines-health-check\\check-pipeline-health.ps1\n\n# VS PR status (checks active non-Experimental PRs and last merged PR)\n$prJson = & .\\.github\\skills\\pipelines-health-check\\check-vs-pr-status.ps1\n\n# VMR codeflow status (checks open codeflow PRs from msbuild and their dotnet-unified-build runs)\n$vmrJson = & .\\.github\\skills\\pipelines-health-check\\check-vmr-codeflow.ps1\n```\n\nAll scripts write progress messages to stderr (`Write-Host`) and the JSON result to stdout.\n\nThe scripts sanitize error messages (stripping control characters, truncating to 500 chars) so the JSON output can be parsed directly with `ConvertFrom-Json` without additional cleanup.\n\n### Step 2: Present the overview table IMMEDIATELY\n\nParse the JSON outputs and render status overview tables to the user **before** doing any deeper investigation. This gives the user instant visibility.\nPresent ALL tables - for both pipelines and for the VS insertion PRs. Do not omit any of those unless explicitly asked by user just for some specific overview.\n\n#### Pipeline Health Table\n\nFor each pipeline in the JSON output, render one row:\n\n| Pipeline | Last Success | Age | Recent Runs | Status |\n|----------|-------------|-----|-------------|--------|\n| {pipelineName} ({pipelineId}) | {lastSuccessfulRun.finishTime} | {lastSuccessfulRun.ageHours}h | emoji sequence | status emoji + label |\n\n**Recent Runs column:** Show an emoji for each run in `recentRuns` array (newest first):\n- `✅` for `succeeded`\n- `❌` for `failed`\n- `⏳` for `inProgress`\n- `⚪` for `canceled` or other\n\n**Status column** — derive from `healthSummary` and `lastSuccessfulRun.ageHours`:\n- `✅ HEALTHY` — healthSummary starts with \"HEALTHY\"\n- `⚠️ FLAKY` — healthSummary starts with \"FLAKY\"\n- `🔴 UNHEALTHY` — healthSummary starts with \"UNHEALTHY\"\n- Add `⚠️` if ageHours > 24, `🔴` if ageHours > 48 (even if some runs succeed, stale success is a concern)\n\n#### VS Insertion PRs Table (non-Experimental)\n\nFor each PR in the `prs` array:\n\n| PR | Title | Checks ✅ | Checks ⏳ | Checks ❌ | Status |\n|----|-------|-----------|-----------|-----------|--------|\n| [{id}](url) | {title} (truncated) | {checks.succeeded} | {checks.pending} | {checks.failed} | status |\n\n**Status column:**\n- `🔴 Failing` — if `actionNeeded` is true (has failed required checks)\n- `⏳ Running` — if `checks.pending > 0` and no failures\n- `✅ Green` — if all checks succeeded or notApplicable\n\n#### Last Merged Insertion Row\n\n| Last Merged PR | Date | Age | Status |\n|---------------|------|-----|--------|\n| [{lastMergedPr.id}](lastMergedPr.url) | {lastMergedPr.closedDate} | {ageDays} days | status |\n\n**Status:**\n- `✅ Recent` — ageHours ≤ 48 (≤ 2 business days)\n- `⚠️ Getting stale` — ageHours > 48 and ≤ 96\n- `🔴 Stale insertion` — ageHours > 96 (> 4 business days)\n\n**Note on weekends:** When computing business-day age, be aware that weekends inflate the hour count. If today is Monday and the last merge was Friday, that's ~72h but only 1 business day. Mention this nuance to the user if the age seems borderline.\n\n#### VMR Codeflow PRs Table\n\nFor each codeflow PR in the `codeflowPRs` array from `$vmrJson`:\n\n| Codeflow PR | Age | Pipeline Runs | Upstream PRs | Status |\n|-------------|-----|---------------|--------------|--------|\n| [#{prNumber}](prUrl) | {prAge}h | emoji sequence from pipelineRuns | count | status emoji + label |\n\n**Pipeline Runs column:** Show an emoji for each run in `pipelineRuns` (newest first):\n- `✅` for `result == \"succeeded\"`\n- `❌` for `result == \"failed\"`\n- `⏳` for `status == \"inProgress\"`\n- `⚪` for other/no runs\n\n**Status column** — derive from `healthSummary`:\n- `✅ HEALTHY` — healthSummary starts with \"HEALTHY\"\n- `🔄 IN PROGRESS` — healthSummary starts with \"IN_PROGRESS\" or \"RETRYING\"\n- `🔴 FAILING` — healthSummary starts with \"UNHEALTHY\"\n- `⚠️ MIXED` — healthSummary starts with \"MIXED\"\n- `❓ UNKNOWN` — no pipeline runs found\n\n**If a PR has failures**, also render a failure details sub-table:\n\n| Failed Job | Failed Task | Error Category | Related Upstream PRs |\n|------------|------------|----------------|---------------------|\n| {job.name} | {task.name} | {category} | PR links from failureCorrelation |\n\nThe `failureCorrelation` array maps each failed build to error categories and potentially related upstream PRs (matched by title keywords). This helps quickly identify which msbuild change likely caused a VMR build failure.\n\n**Upstream PRs list:** For each codeflow PR, list the included upstream msbuild PRs:\n\n> **Included changes:** [#13175](url) Add App Host Support, [#13306](url) IBuildEngine callbacks, ...\n\n### Step 3: Identify problems\n\nAfter rendering the table, build a list of distinct problems. A \"problem\" is any of:\n\n1. **Pipeline failure** — A pipeline whose latest run on main failed, especially if `lastSuccessfulRun.ageHours > 24`\n2. **PR check failure** — An active non-Experimental PR that has `actionNeeded: true` (failed required checks)\n3. **Stale insertion** — `lastMergedPr.ageHours > 48` (no successful insertion in >2 business days)\n4. **All checks pending** — A PR where all checks are still pending/queued (may indicate a stuck pipeline or queue issue)\n5. **VMR codeflow failure** — A codeflow PR whose `healthSummary` starts with \"UNHEALTHY\" (dotnet-unified-build failing)\n6. **VMR codeflow stale** — A codeflow PR older than 48 hours with no successful pipeline run\n\nIf there are **no problems**, report `✅ ALL CLEAR — pipelines healthy, PRs on track, insertions flowing, VMR codeflow green` and stop. Do not proceed to Phase 2.\n\n## Phase 2: Investigate Problems via Subagents\n\nFor **each distinct problem** identified in Step 3, launch a **separate subagent** to perform DEEP, DETAILED investigation (use `#tool:agent/runSubagent` to run the investigation tasks). Fire them in parallel when possible. Use the <agent> template below to seed them. \n\n<subagent>\n\n### Subagent prompt templates\n\n#### For pipeline failures\n\n```\nInvestigate why the Azure DevOps pipeline \"{pipelineName}\" (ID: {pipelineId}) is failing.\n\nRecent failed runs on branch {branch}:\n{for each failed run, list: Run ID, start time, URL, and the failedTasks with their error messages}\n\nLast successful run: {lastSuccessfulRun.finishTime} ({ageHours} hours ago)\nURL: {lastSuccessfulRun.url}\n\nTasks:\n1. Categorize each failure as one of:\n   - BUILD ERROR: compilation failures, test failures, task execution errors in MSBuild code\n   - CONFIG/PERMISSION: signing errors, NuGet authentication, certificate issues, feed access\n   - INFRA/TRANSIENT: errors indicating unavailability or outage of services or resources\n2. Check if all recent failures share the same root cause or if there are different issues\n3. If infra/transient: suggest retrying the pipeline (provide the pipeline URL)\n4. If build error: \n  - Check the `For build errors` section below on how to investigate build errors with binlogs\n  - identify which component/task is failing and check recent commits to main to try to identify offending one.\n5. If infrastructure issues:\n  - Try to distill the exact reason for the issue, check if there are other failing pipelines with the same issue or any open bugs for the issue.\n  - **Use WorkIQ** to find the owning team and contacts. Check if `workiq` CLI is available (`workiq version`).\n    - If available, run: `workiq ask -q \"Who owns the {failing service/task name} service in Microsoft DevDiv? Who should be contacted about {brief error description}?\"`\n    - Include the WorkIQ response in your findings — it typically returns team names, distribution lists, contact people, and escalation paths.\n    - You can also ask WorkIQ about known outages: `workiq ask -q \"Are there any known outages or incidents for {service name} in Azure DevOps?\"`\n    - If WorkIQ is NOT available, note this in your report and suggest the user install it:\n      ```\n      npm install -g @microsoft/workiq --registry https://registry.npmjs.org\n      workiq accept-eula\n      ```\n  - Put together a concise overview of the issue, along with links to the failure messages, the owning team/contacts from WorkIQ, and suggested next steps.\n\nReturn: A comprehensive root cause analysis with category, explanation, links to failure messages, ownership info (from WorkIQ if available), and recommended action.\n```\n\n#### For PR check failures\n\n```\nInvestigate failing checks on VS insertion PR #{prId}: \"{prTitle}\"\nPR URL: {prUrl}\n\nFailed checks:\n{for each item in checks.failedChecks: genre, name, description, isRequired}\n\nPending checks (still running):\n{for each item in checks.pendingChecks: genre, name, description, isRequired}\n\nPipeline health context:\n{brief summary of pipeline health from Phase 1 — are pipelines also failing?}\n\nTasks:\n1. Identify which failed checks are required vs optional\n2. If required checks are failing, determine if this could be related to pipeline failures (same root cause)\n3. If checks are just pending/queued, note that they may still be running and suggest waiting\n4. Recommend specific actions: retry checks, investigate pipeline, or wait\n5. If check is failing - try to traverse the chain of called pipelines to the actual error, then: \n  - Check the `For build errors` section below on how to investigate build errors with binlogs\n  - identify which component/task is failing and check recent commits to msbuild main to try to identify offending one.\n\nReturn: Which checks need attention, likely cause, and recommended action.\n```\n\n#### For stale insertion\n\n```\nInvestigate why MSBuild insertions into VS appear stale.\n\nLast successfully merged non-Experimental PR: #{lastMergedPr.id} \"{lastMergedPr.title}\"\n  Merged: {lastMergedPr.closedDate} ({ageDays} days ago)\n  URL: {lastMergedPr.url}\n\nCurrently active non-Experimental PRs:\n{for each PR: id, title, url, checks summary, actionNeeded}\n\nPipeline health:\n{brief pipeline health summary}\n\nTasks:\n1. Check if there are active non-Experimental PRs waiting — if none, the issue may be that no insertion was triggered\n2. If there are active PRs with failing checks, identify if those failures are blocking the insertion\n3. If there are active PRs with all checks pending, they may just need time\n4. Correlate with pipeline health — if the CI pipeline is broken, insertions can't succeed\n5. Recommend specific actions to unblock\n\nReturn: Explanation of why insertion appears stuck and what to do about it.\n```\n\n#### For VMR codeflow failures\n\n```\nInvestigate failing dotnet-unified-build pipeline for VMR codeflow PR #{prNumber}: \"{prTitle}\"\nPR URL: {prUrl}\nPR Branch: {prBranch}\n\nThis is a codeflow PR that brings MSBuild source changes into the dotnet/dotnet VMR (Virtual Monolithic Repository).\nThe dotnet-unified-build pipeline runs in the dnceng-public/public Azure DevOps org.\n\nFailed pipeline runs:\n{for each failed run in pipelineRuns: buildId, buildNumber, URL, stages, failedJobs with their failedTasks and errors}\n\nFailure categories detected: {failureCorrelation[].categories}\n\nUpstream MSBuild PRs included in this codeflow:\n{for each PR in upstreamPRs: number, title, url, merged status}\n\nFailure-to-change correlation (from script):\n{for each item in failureCorrelation: buildId, categories, relatedUpstreamPRs}\n\nTasks:\n1. For each failed job, examine the error messages and categorize:\n   - TASK_HOST: MSB4216 errors about MSBuild not finding the task host executable — typically caused by changes to MSBuild's app host, node launching, or SDK layout\n   - SOURCE_BUILD_TASK_HOST: Same as TASK_HOST but in the source-only build (CentOS offline) — the previously-source-built SDK doesn't have the expected MSBuild executable\n   - COMPILATION: CS/VB/FS compilation errors — a code change broke the build\n   - BUILD_COMMAND: MSB3073 \"exited with code N\" — a build script or test failed\n   - NUGET_AUTH/SIGNING/TIMEOUT/RESOURCE: Infrastructure issues\n2. Use the failure-to-change correlation to identify which upstream MSBuild PR most likely caused each failure:\n   - Check if the error is in an area touched by one of the upstream PRs\n   - If TASK_HOST errors: look for PRs touching NodeLauncher, NodeProvider, task host, app host, or BuildEnvironmentHelper\n   - If COMPILATION errors: check which files the upstream PRs modified and whether any could cause the compilation break\n   - If test failures: identify which test is failing and which upstream PR likely affects that code path\n3. Check if there is an in-progress retry build that might resolve the issue\n4. If the failure appears to be an infrastructure issue (not caused by MSBuild changes), note that\n5. For build errors, check the `For build errors` section on how to investigate with binlogs\n6. Recommend specific actions:\n   - If an upstream PR is clearly at fault, suggest reverting it or filing a fix\n   - If a VMR-side fix is needed (e.g., SDK layout change), describe what needs to change\n   - If a retry might help (transient infra), suggest re-running the pipeline\n   - If the issue is already being retried (in-progress build), suggest waiting\n\nReturn: Root cause analysis mapping each failure to the likely upstream PR, with explanation, links, and recommended action.\n```\n\n#### For build errors\n\nTasks:\n1. Try to find a .binlog file(s) in the build or step artifacts and fetch it\n2. Ensure to acquire the [binlog-failure-analysis skill](https://github.com/ViktorHofer/dotnet-skills/blob/main/msbuild-skills/skills/binlog-failure-analysis/SKILL.md) together with the binlog-mcp (spawn via `dnx -y baronfel.binlog.mcp@0.0.13`)\n3. Use the binlog analysis skill and mcp to analyse the binlog(s) you found and analyse problems from those\n\n</subagent>\n\n## Phase 3: Final Report\n\nAfter all subagent results return, present the findings below the overview table under a **\"🔍 Problems & Recommendations\"** heading. For each problem:\n\n```markdown\n### Problem: {brief title}\n**Category:** {INFRA | BUILD | CONFIG | PR_CHECKS | STALE_INSERTION | VMR_CODEFLOW}\n**Details:** {subagent's explanation}\n**Ownership:** {owning team, contacts, DL from WorkIQ — include only for INFRA/CONFIG issues}\n**Recommended Action:** {subagent's recommendation}\n```\n\nIf all problems are infra/transient, add a note: *\"All current issues appear to be infrastructure-related. Consider retrying the pipelines and checking again in 30 minutes.\"*\n\n## Troubleshooting\n\n### \"az: command not found\" or \"az account get-access-token\" fails\nThe `az` CLI is not installed or not authenticated. Run `az login` first.\n\n### Scripts return empty arrays\n- Check that you have access to the DevDiv organization\n- The branch filter defaults to `main` — if checking a different branch, pass `-Branch <name>` to the pipeline script\n\n### PR statuses all show as \"pending\"\nThis is normal for newly created PRs. The checks take time to queue and run. If checks are pending for more than a few hours, this may indicate a stuck pipeline or queue issue.\n\n### Timeout or rate limiting\nIf the scripts take a long time or fail with 429 errors, Azure DevOps may be rate-limiting. Wait a minute and retry.\n\n### WorkIQ not found or EULA not accepted\nIf `workiq version` fails, install it:\n```powershell\nnpm install -g @microsoft/workiq --registry https://registry.npmjs.org\nworkiq accept-eula\n```\nNote: If your `.npmrc` redirects the `@microsoft` scope to GitHub Packages, use `--registry https://registry.npmjs.org` to override, or pass `--userconfig` pointing to a clean `.npmrc`.\n\n### WorkIQ returns empty or unhelpful results\nWorkIQ queries Microsoft 365 data (Outlook, Teams, SharePoint). Results depend on your account's access and the data available in your tenant. Try rephrasing the question or being more specific about the service name. Example queries that work well:\n- `workiq ask -q \"Who owns the MicroBuild service in Microsoft?\"`\n- `workiq ask -q \"Who owns the CloudBuild signing service in DevDiv?\"`\n- `workiq ask -q \"Who should I contact about NuGet feed authentication failures in Azure DevOps?\"`\n\n### VMR codeflow script returns no PRs\n- Verify there are actually open codeflow PRs: check `https://github.com/dotnet/dotnet/pulls?q=is:pr+is:open+\"Source+code+updates+from+dotnet/msbuild\"`\n- Verify `gh` CLI is authenticated: run `gh auth status`\n\n### VMR pipeline runs not found\n- The dotnet-unified-build pipeline runs in `dnceng-public/public`, which is publicly accessible\n- If no runs appear, the pipeline may not have been triggered yet for that PR\n- Codeflow PRs may take a few minutes after creation before CI triggers\n\n### JSON output too large or contains unexpected characters\n- Error messages from Azure DevOps timelines can contain Windows paths, control characters, and multi-KB stack traces\n- The scripts sanitize and truncate these to 500 characters — if you still see issues, check that you're running the latest version of the scripts\n- Use `ConvertFrom-Json` in PowerShell or `json.loads()` in Python to parse the output; avoid manual string manipulation\n","frontmatter":{"name":"pipelines-health-check","description":"Check health of MSBuild CI pipelines, VS repo PR insertion statuses, and VMR codeflow PRs. Use when asked about pipeline health, build failures, infrastructure issues, CI status, insertion PR status, VMR codeflow status, or for periodic health monitoring."},"isInternal":false,"tokens":5025,"sizeBytes":20542},{"name":"SKILL.md","path":".github/skills/project-management/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/project-management/SKILL.md","title":"project-management","category":"anthropic-skill","format":"markdown","content":"---\nname: project-management\ndescription: 'GitHub issue and project-board management for the dotnet/msbuild repo. Use when asked to file/triage/update issues, post comments, amend issue bodies, move sprints, bulk-update project board fields, or audit items by sprint/status/assignee.'\nargument-hint: 'Manage issues and project board entries for dotnet/msbuild.'\n---\n\n# Project Management for dotnet/msbuild\n\nThis skill covers everything you need to manage issues and the project board for the MSBuild repository: querying, commenting, editing, creating issues, and bulk operations on the project board (sprints, status, assignees, etc.).\n\n## Where things live\n\n- **Repository:** `dotnet/msbuild` — all issues, PRs, and discussions.\n- **Project board:** [`dotnet/projects/117`](https://github.com/orgs/dotnet/projects/117) — title \"MSBuild\". This is the canonical project board for the team. Most issues created in `dotnet/msbuild` are auto-added or manually added here for sprint planning.\n- **Sprints** are tracked via the **Sprint** iteration field on project 117. Iteration titles use the format `2026 May I`, `2026 May II`, `2026 June I`, etc. (roughly two-week iterations).\n\n> Note: there are/were other auxiliary project boards for specific initiatives. Use project 117 unless the user explicitly points at another one.\n\n### Stable identifiers for project 117\n\n| Identifier | Value |\n|---|---|\n| Project node ID | `PVT_kwDOAIt-yc4ABM5F` |\n| Sprint field ID (iteration) | `PVTIF_lADOAIt-yc4ABM5FzgAtMGI` |\n\nThese IDs are stable but can be re-derived if the project is ever recreated or migrated:\n```bash\n# Project node ID\ngh api graphql -f query='{ organization(login:\"dotnet\") { projectV2(number:117) { id } } }'\n\n# All field IDs (Sprint, Status, Area, Priority, …)\ngh project field-list 117 --owner dotnet --format json | jq '.fields[] | {id,name,type}'\n```\n\n## Tooling preference\n\nFor GitHub work, prefer `gh` CLI over MCP tools (per repo convention).\n\n- Use `gh issue ...` for issue-level operations (create / view / comment / edit / close).\n- Use `gh project ...` for project-board CRUD.\n- Use `gh api graphql` for anything `gh` doesn't expose directly (sprint values per item, project-level field values, bulk reads).\n\nThe default `gh` token may not include the `project` scope. If a `gh project ...` write call fails with a scope error, ask the user to run:\n```bash\ngh auth refresh -s project\n```\n\n## Common single-issue actions\n\n```bash\n# View\ngh issue view 13315 --repo dotnet/msbuild --json title,body,state,assignees,labels\n\n# Comment (use --body-file to avoid shell-quoting traps with multi-line markdown)\ngh issue comment 13315 --repo dotnet/msbuild --body-file ./comment.md\n\n# Amend body (overwrite — there is no append; use view → edit → write)\ngh issue edit 13597 --repo dotnet/msbuild --body-file ./new-body.md\n\n# Create\ngh issue create --repo dotnet/msbuild --title \"Title\" --body-file ./body.md\n\n# Add labels / assignees\ngh issue edit 13315 --repo dotnet/msbuild --add-label \"Area: Server\" --add-assignee someuser\n```\n\nWhen drafting comments / bodies that summarize internal discussions:\n- Use neutral, team-voice language (\"the team agreed\", \"open question\").\n- Scrub names, \"X said Y\", room/agent identifiers, and anything resembling internal politics.\n- Always write to a file and use `--body-file`. Don't try to inline multi-line markdown.\n\n## Querying issue → project field values (sprint, status, project-level assignees)\n\n`gh issue view` does **not** return project-board field values. Use GraphQL via the `repository → issue → projectItems` path:\n\n```bash\ngh api graphql -f query='\nquery {\n  r: repository(owner:\"dotnet\", name:\"msbuild\") {\n    issue(number: 13315) {\n      id\n      projectItems(first: 5) {\n        nodes {\n          id\n          project { number title }\n          sprint:  fieldValueByName(name: \"Sprint\")  { ... on ProjectV2ItemFieldIterationValue   { title iterationId } }\n          status:  fieldValueByName(name: \"Status\")  { ... on ProjectV2ItemFieldSingleSelectValue { name optionId } }\n        }\n      }\n    }\n  }\n}'\n```\n\n> `fieldValueByName` returns `null` when the field has no value set on that item — handle accordingly.\n\n## Bulk / board-wide queries\n\nThe `gh project item-list` CLI command has internal limits and is unreliable for large boards (project 117 is in the thousands of items). **Use GraphQL with pagination.**\n\n```python\nimport subprocess, json\n\nPROJECT_NUMBER = 117\nORG = \"dotnet\"\n\nresults = []\ncursor = None\nwhile True:\n    after = f', after: \"{cursor}\"' if cursor else ''\n    query = f'''query {{\n      organization(login: \"{ORG}\") {{\n        projectV2(number: {PROJECT_NUMBER}) {{\n          items(first: 100{after}) {{\n            pageInfo {{ hasNextPage endCursor }}\n            nodes {{\n              id\n              fieldValues(first: 20) {{\n                nodes {{\n                  ... on ProjectV2ItemFieldIterationValue {{\n                    field {{ ... on ProjectV2IterationField  {{ name }} }}\n                    title\n                  }}\n                  ... on ProjectV2ItemFieldUserValue {{\n                    field {{ ... on ProjectV2Field           {{ name }} }}\n                    users(first: 10) {{ nodes {{ login }} }}\n                  }}\n                  ... on ProjectV2ItemFieldSingleSelectValue {{\n                    field {{ ... on ProjectV2SingleSelectField {{ name }} }}\n                    name\n                  }}\n                }}\n              }}\n              content {{\n                ... on Issue        {{ number title }}\n                ... on PullRequest  {{ number title }}\n                ... on DraftIssue   {{ title }}\n              }}\n            }}\n          }}\n        }}\n      }}\n    }}'''\n    try:\n        r = subprocess.run(['gh','api','graphql','-f',f'query={query}'],\n                           capture_output=True, text=True, check=True)\n    except subprocess.CalledProcessError as e:\n        raise RuntimeError(f\"GraphQL query failed: {e.stderr}\") from e\n    page = json.loads(r.stdout)['data']['organization']['projectV2']['items']\n    for node in page['nodes']:\n        sprint = status = None\n        assignees = []\n        for fv in node.get('fieldValues', {}).get('nodes', []):\n            if not fv:                              # null entries for unset fields\n                continue\n            fname = (fv.get('field') or {}).get('name', '')\n            if fname == 'Sprint':\n                sprint = fv.get('title')\n            elif fname == 'Status':\n                status = fv.get('name')\n            elif fname == 'Assignees':\n                assignees = [u['login'] for u in (fv.get('users') or {}).get('nodes', [])]\n        results.append({'id': node['id'], 'sprint': sprint, 'status': status,\n                        'assignees': assignees, 'content': node.get('content')})\n    if not page['pageInfo']['hasNextPage']:\n        break\n    cursor = page['pageInfo']['endCursor']\n```\n\n## Updating project-board field values\n\n### Set a sprint (iteration) — first look up iteration IDs\n\nIteration IDs are short hex strings (e.g., `303c2425`). They are **not** the same across projects; always look them up from project 117.\n\n```bash\ngh api graphql -f query='\nquery {\n  node(id: \"PVTIF_lADOAIt-yc4ABM5FzgAtMGI\") {\n    ... on ProjectV2IterationField {\n      configuration {\n        iterations           { id title startDate }\n        completedIterations  { id title startDate }\n      }\n    }\n  }\n}' | jq '.data.node.configuration | (.iterations + .completedIterations)[] | {id,title}'\n```\n\n### Single-item field update — `gh project item-edit`\n\n```bash\n# Iteration / sprint\ngh project item-edit \\\n  --project-id PVT_kwDOAIt-yc4ABM5F \\\n  --id <PROJECT_ITEM_ID> \\\n  --field-id PVTIF_lADOAIt-yc4ABM5FzgAtMGI \\\n  --iteration-id <ITERATION_ID>\n\n# Single-select (Status, Area, Priority): use --single-select-option-id\n# Text: --text  | Number: --number  | Date: --date YYYY-MM-DD\n```\n\n### Single-item field update — GraphQL mutation (works the same)\n\n```bash\ngh api graphql -f query='\nmutation {\n  updateProjectV2ItemFieldValue(input: {\n    projectId: \"PVT_kwDOAIt-yc4ABM5F\",\n    itemId:    \"<PROJECT_ITEM_ID>\",\n    fieldId:   \"PVTIF_lADOAIt-yc4ABM5FzgAtMGI\",\n    value:     { iterationId: \"<ITERATION_ID>\" }\n  }) { projectV2Item { id } }\n}'\n```\n\n### Adding a freshly created issue to project 117\n\nA newly-created issue is not always auto-added. To add and set its sprint:\n\n```bash\nISSUE_NODE_ID=$(gh api graphql -f query='\n  query { repository(owner:\"dotnet\",name:\"msbuild\") { issue(number:13707) { id } } }' \\\n  | jq -r '.data.repository.issue.id')\n\nITEM_ID=$(gh api graphql -f query='\n  mutation { addProjectV2ItemById(input:{\n      projectId:\"PVT_kwDOAIt-yc4ABM5F\",\n      contentId:\"'\"$ISSUE_NODE_ID\"'\"\n  }) { item { id } } }' | jq -r '.data.addProjectV2ItemById.item.id')\n\ngh api graphql -f query='\n  mutation { updateProjectV2ItemFieldValue(input:{\n      projectId:\"PVT_kwDOAIt-yc4ABM5F\",\n      itemId:\"'\"$ITEM_ID\"'\",\n      fieldId:\"PVTIF_lADOAIt-yc4ABM5FzgAtMGI\",\n      value:{iterationId:\"<ITERATION_ID>\"}\n  }) { projectV2Item { id } } }'\n```\n\n## Common errors and gotchas\n\n### 1. `Field 'fieldValueByName' has an argument conflict`\n\n`gh api graphql --paginate` (and several jq-style aliases) re-uses field names. If you call `fieldValueByName(name:\"Sprint\")` and `fieldValueByName(name:\"Status\")` in the same selection set without aliases, you'll get:\n\n```\nField 'fieldValueByName' has an argument conflict: {name:\"Sprint\"} or {name:\"Status\"}?\n```\n\nFix: alias each call.\n\n```graphql\nsprint: fieldValueByName(name: \"Sprint\")  { ... }\nstatus: fieldValueByName(name: \"Status\")  { ... }\n```\n\n### 2. `gh api graphql --paginate` produces concatenated JSON, not one document\n\nEach page is emitted as a separate JSON object on stdout; piping straight into `jq` fails with \"Invalid numeric literal\". Either:\n- paginate manually in a loop (preferred for large reads), or\n- use `jq --slurp` to read the stream as an array.\n\n### 3. \"Selections can't be made directly on unions\"\n\n`field` on a project field value is a union; you can't select `name` directly. Use a typed inline fragment per concrete field type (`ProjectV2Field`, `ProjectV2IterationField`, `ProjectV2SingleSelectField`, …).\n\n### 4. Issue-level assignees ≠ project-board \"Assignees\" column\n\nThe board's \"No Assignees\" slice is driven by the **project-level Assignees field**, not by `Issue.assignees`. They can diverge. When filtering by board state, always read `ProjectV2ItemFieldUserValue` from `fieldValues`.\n\n### 5. `null` entries in `fieldValues.nodes`\n\nGraphQL returns `null` for items where a particular field is unset. Always guard `if not fv: continue` before dereferencing.\n\n### 6. DraftIssue items lack `number` / `repository`\n\n`content` may be a `DraftIssue` that only exposes `title`, `body`, `assignees`. Code that joins to issue numbers must handle `None`.\n\n### 7. `gh project item-list` is unreliable for large boards\n\nIt silently truncates. Always use the GraphQL pagination loop above instead.\n\n### 8. Missing `project` scope on `gh` token\n\nReads work; writes (`gh project item-edit`, the `addProjectV2ItemById` / `updateProjectV2ItemFieldValue` mutations) fail. Have the user run `gh auth refresh -s project`.\n\n### 9. `gh issue edit --body` overwrites — there is no append\n\nTo \"append\" to a body, fetch with `gh issue view --json body`, append, write back with `--body-file`. For status updates, prefer a comment.\n\n## Sanitization checklist for comments / body edits\n\nWhen summarizing meetings or chat threads into GitHub:\n\n- ✅ Use team voice (\"the team agreed\", \"open question\", \"follow-up needed\").\n- ✅ Keep objective, decision-relevant content (problem statement, scope, exit criteria, repro steps, numbers).\n- ❌ No personal names or \"X said / Y replied\".\n- ❌ No full email addresses of external contributors (use GitHub handles instead).\n- ❌ No internal-only identifiers (room names, recording IDs, agent fleet identifiers, partner-team contractor names).\n- ❌ No speculation about other teams' motives or anything resembling internal politics.\n- ❌ No confidential roadmap dates or unreleased product details unless already public.\n- Always show the user the proposed comment / body before posting (unless they've explicitly preapproved).\n\n## Common workflows\n\n| Goal | Sketch |\n|---|---|\n| Add a status comment to N issues | `for n in 13315 13702 …; do gh issue comment $n --repo dotnet/msbuild --body-file ./c-$n.md; done` |\n| Replace a one-line issue body with a full Context / Exit-criteria / Out-of-scope spec | `gh issue edit <n> --repo dotnet/msbuild --body-file ./body.md` |\n| Move all unassigned items from sprint X to sprint Y | GraphQL pagination loop above + `gh project item-edit --iteration-id` per item |\n| File a new issue and put it in the current sprint | `gh issue create` → `addProjectV2ItemById` → `updateProjectV2ItemFieldValue` |\n| Audit \"what's in the current sprint that has no owner\" | GraphQL pagination loop, filter on `Sprint == \"<current>\"` and empty `Assignees` |\n","frontmatter":{"name":"project-management","description":"GitHub issue and project-board management for the dotnet/msbuild repo. Use when asked to file/triage/update issues, post comments, amend issue bodies, move sprints, bulk-update project board fields, or audit items by sprint/status/assignee.","argument-hint":"Manage issues and project board entries for dotnet/msbuild."},"isInternal":false,"tokens":3246,"sizeBytes":13075},{"name":"SKILL.md","path":".github/skills/release/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/release/SKILL.md","title":"release","category":"anthropic-skill","format":"markdown","content":"---\nname: release\ndescription: 'Orchestrate an MSBuild release: create the tracking issue, branch, configure DARC channels and subscriptions, bump version in main, final-brand the release branch, insert into VS, and publish post-GA. Covers the full monthly release lifecycle aligned with VS shipping cadence.'\nargument-hint: 'Specify which phase to execute (0-5) and provide required inputs (THIS_RELEASE_VERSION, NEXT_VERSION, etc.)'\n---\n\n# MSBuild Release Orchestration\n\nThis skill guides an agent through the MSBuild release process defined in [`documentation/release-checklist.md`](../../../documentation/release-checklist.md). The checklist is the single source of truth — this skill provides context on how to execute it.\n\n## Overview\n\nMSBuild is a **component** that gets inserted into Visual Studio. VS ships monthly; MSBuild must branch and prepare its bits **before** VS is ready to take them. See the [release process doc](../../../documentation/release.md#how-msbuild-releases-flow-into-vs) for the full timeline diagram.\n\nThe insertion pipeline routes MSBuild branches to VS branches:\n- `main` → VS `main` (daily canary)\n- `vs*` release branch → VS `main` (replaces `main` → `main` after branch snap)\n\nVS handles the progression from `main` → `rel/insiders` → `rel/stable` on its own schedule. MSBuild's responsibility is to have final-branded bits in VS `main` before the insiders snap date.\n\nEach monthly VS release produces:\n- A new `vs*` branch from `main`\n- Final branding on that branch\n- A version bump in `main`\n- DARC channel/subscription updates\n- A VS insertion\n- Post-GA publishing to nuget.org and docs\n\nThe process is organized into **6 timeline-gated phases** (0–5), each with an explicit trigger.\n\n> **Execution model**: This skill is designed for an **interactive Copilot session**. The agent walks through each phase step-by-step, but every command that modifies state (git push, DARC writes, pipeline changes, PR creation) requires **user approval** before execution. Read-only queries (DARC get-*, git log, etc.) can run without approval.\n\n## Required Inputs\n\nBefore starting any phase, ensure you have these values (the user must provide them — version increments are irregular and cannot be computed):\n\n| Input | Example | How to determine |\n|---|---|---|\n| `PREVIOUS_RELEASE_VERSION` | `18.9` | Previous release branch version: the highest `vs*` version lower than `THIS_RELEASE_VERSION` (list candidates with `git branch -r --list 'origin/vs*'`). |\n| `PREVIOUS_RELEASE_EXACT_VERSION` | `18.9.6` | The version the previous release actually shipped as. From the previous release's tracking issue, or `git tag --list 'v18.9.*'`. Used by Phase 5.3a. |\n| `THIS_RELEASE_VERSION` | `18.10` | Current `VersionPrefix` in `eng/Versions.props` (drop `.0`) |\n| `NEXT_VERSION` | `18.11` | User-provided — not computable from current version |\n| `BRANCH_SNAP_DATE` | `YYYY-MM-DD` | From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) — when MSBuild branches `vs*` from main, insertion targets VS `main` |\n| `INSIDERS_SNAP_DATE` | `YYYY-MM-DD` | From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) — when VS snaps `main` → `rel/insiders`; final-branded bits must be in VS `main` before this |\n| `STABLE_SNAP_DATE` | `YYYY-MM-DD` | From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) — when VS promotes `rel/insiders` → `rel/stable` |\n| `VS_SHIP_DATE` | `YYYY-MM-DD` | When VS ships publicly (GA) — triggers post-release tasks |\n| `PACKAGE_VALIDATION_BASELINE_VERSION` | `18.10.0-1.26378.2` | See [How to determine `PACKAGE_VALIDATION_BASELINE_VERSION`](#how-to-determine-package_validation_baseline_version) below — non-trivial: most \"obvious\" picks are wrong. |\n\n> Version examples above track the current cycle (`eng/Versions.props` `VersionPrefix` is `18.10.0`). Dates are intentionally shown as a format only — always read the real ones from the VS-Dates wiki.\n\n### How to determine `PACKAGE_VALIDATION_BASELINE_VERSION`\n\n**The value is the latest `{{THIS_RELEASE_VERSION}}.0-<label>.<shortDate>.<rev>` MSBuild package that is both:**\n\n_`<label>` is `PreReleaseVersionLabel` from `eng/Versions.props` (currently `1`), so the versions look like `18.11.0-1.26426.2`. Do **not** assume the legacy `-preview-NNNNN-NN` shape — MSBuild no longer produces it._\n\n1. **Published on the public [dotnet-tools feed](https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-tools)** — this is the feed the official build publishes to and that ApiCompat restores baselines from. If the version isn't here, ApiCompat fails with `NU1102`.\n2. **Produced from a commit reachable from `vs{{THIS_RELEASE_VERSION}}`** — i.e. a commit on `vs{{THIS_RELEASE_VERSION}}`, or the `main` commit `vs{{THIS_RELEASE_VERSION}}` was branched from.\n\n**Two tempting wrong answers — and why they're wrong:**\n\n| Wrong pick | Why it fails |\n|---|---|\n| ❌ The release-versioned `{{THIS_RELEASE_VERSION}}.X` package that ships in VS / on nuget.org | Since [#14277](https://github.com/dotnet/msbuild/pull/14277) release branches build and insert **prerelease** versions, exactly like `main`; the release-versioned variants are produced by NuGetRepack at manual publish time. They never exist on the public CI feed, so ApiCompat cannot restore them. |\n| ❌ Blindly the most recent `{{THIS_RELEASE_VERSION}}.0-*` on `dotnet-tools` | After `vs{{THIS_RELEASE_VERSION}}` branches, `main` keeps producing `{{THIS_RELEASE_VERSION}}.0-*` until **this** main-bump PR merges — so the most recent feed entries may be `{{NEXT_VERSION}}`-content builds wearing `{{THIS_RELEASE_VERSION}}` branding. Picking one drifts the API baseline forward and silently hides real compat breaks. |\n\n**Timing caveat:** the branch-point build publishes to `dotnet-tools` asynchronously. If you branch and bump on the same day, the script may report the correct candidate as `[NOT on feed]` — wait for publication and re-run rather than substituting an older build.\n\n**Procedure:** run the helper — it does the whole resolution mechanically (requires `az login` with devdiv access):\n\n```\npwsh ./scripts/Get-PackageValidationBaseline.ps1 -ThisReleaseVersion {{THIS_RELEASE_VERSION}}\n# -> prints e.g. 18.11.0-1.26426.2\n```\n\nIt computes `git merge-base origin/main origin/vs{{THIS_RELEASE_VERSION}}`, finds the matching successful build in [pipeline 9434](https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434), derives the package version from the OfficialBuildId, and verifies it on the dotnet-tools feed. If it fails, read the script's own `.DESCRIPTION` header for the manual equivalent rather than reproducing it here.\n\n### Prerequisites\n- gh cli\n- az cli\n- darc cli — Arcade enforces a minimum version; if `darc` refuses with a \"below the minimum required version\" error, run `.\\eng\\common\\darc-init.ps1`\n\n## Phase Summary\n\n| Phase | Trigger | Key Actions |\n|---|---|---|\n| **0: Instantiate** | User-initiated | Validate inputs, create GitHub tracking issue |\n| **1: Branch & Prepare** | `BRANCH_SNAP_DATE` | Create `vs*` branch, DARC channel setup (batched PR), **audit all `vs*` branches for retirement** |\n| **2: DARC Subscription Updates** | Phase 1 branch exists (`vs*` created) | Retarget `main`-targeting subs + VMR backflow to next channel, retired-branch cleanup (batched PR), Arcade verify |\n| **3: Bump Main** | Phase 2 ingested and verified | Branding PR in `main` (`VersionPrefix` → next, ApiCompat baseline, refresh OptProf baseline) |\n| **4: Final Branding** | 7 days before `INSIDERS_SNAP_DATE` | Telemetry evidence checks, package API compatibility review, OptProf bootstrap, VS insertion before Insiders snap |\n| **5: Post-GA** | VS shipped (`VS_SHIP_DATE`) | Resolve the exact shipped version (SDK-coupled? SDK wins over VS `rel/stable`), nuget.org publish, docs, GitHub release, Change Waves Learn sync, retro |\n\n## DARC Batching\n\nDARC write commands push to the [maestro-configuration](https://dev.azure.com/dnceng/internal/_git/maestro-configuration) repo. Batch related changes into **one PR**:\n\n1. Choose a branch name like `release/msbuild-{{THIS_RELEASE_VERSION}}`\n2. Add `--configuration-branch <name> --no-pr` to every write command except the last\n3. Last command: use `--configuration-branch <name>` without `--no-pr` to create the PR\n4. Review the diff before merging; disable auto-complete if the PR must remain review-only.\n5. After merging, complete [DARC configuration PR verification](../../../documentation/release-checklist.md#darc-configuration-pr-verification) before dependent release steps.\n\nCheck existing channels and mappings before writing: the previous release normally pre-created the current release's mapping. Skip satisfied writes rather than constructing a redundant batch.\n\nRead-only commands (`get-default-channels`, `get-subscriptions`, `get-channel`) don't need these flags.\n\n**Non-interactive (`-q`).** `darc add-default-channel` / `add-subscription` prompt `y/n` when the target branch does not exist yet (e.g. pre-creating the `vs{{NEXT_VERSION}}` mapping in Phase 1.2c, or adding the new `vs{{THIS_RELEASE_VERSION}}` backflow in Phase 2). `darc delete-subscriptions` always prompts for confirmation. Console input is redirected in an agent session, so the prompt **fails or hangs the command** — always pass `-q` for these writes. Note the verb is `delete-subscriptions` (plural); `delete-subscription` does not exist.\n\n**Phase 2 — forward-flow rotation.** Move subscriptions targeting `main` (`dotnet/dotnet @ main`, `dotnet/fsharp @ main`) to the next channel so those consumers receive next-version builds. Keep VMR `release/*` targets, including `.NET-next` preview bands, on `VS {{THIS_RELEASE_VERSION}}` so the release branch continues to feed its SDK bands.\n\n**Phase 2 — VMR backflow rotation.** Backflow (`dotnet/dotnet → msbuild`, source-enabled) keeps each MSBuild branch's dependencies aligned with its SDK band. When the new `vs{{THIS_RELEASE_VERSION}}` is paired with an SDK band, repoint the `→ main` backflow to the **next** SDK band channel and add backflow from the **outgoing** band channel into the release branch. Skip this for a VS-only release. See checklist steps 2.2b / 2.3f / 2.3g.\n\n## Branch retirement (Phase 1.3)\n\nAudit every release branch to keep required SDK/VS servicing flows active and remove unused dependency automation.\n\n**Branch inventory:** [Maintained branches](../../../documentation/release.md#maintained-branches) is the static list. Update its branch rows, SDK/VS sources, and support deadlines when cutting or retiring branches; cross-check the live DARC mappings, subscriptions, and repository branches in checklist step 1.3a.\n\nCheck both lifecycles because either the SDK or Visual Studio can require a branch:\n\n| Lifecycle | Source |\n|---|---|\n| **SDK band** | [supported .NET versions table](https://learn.microsoft.com/dotnet/core/porting/versioning-sdk-msbuild-vs#supported-net-versions) — maps each feature band to its VS version and support end date |\n| **Visual Studio** | [VS Product Lifecycle and Servicing](https://learn.microsoft.com/visualstudio/releases/2026/servicing-vs) — annual releases and the LTSC table |\n\n**VS 2026 and later:** non-LTSC releases are serviced through Insiders and for one month after Stable release. A new Stable promotion ends VS support for the previous release. LTSC baselines receive one year of servicing after release; use the published LTSC table to identify them.\n\nUse published SDK and VS deadlines for lifecycle decisions, and DARC channels and subscriptions for dependency routing.\n\nApply the **combined rule**: retire a branch paired with an SDK band and a VS version only when both lifecycles agree it is out of support. Check these candidates for automation cleanup:\n\n| Signal | Meaning |\n|---|---|\n| Branch's `VS X.Y` channel has **no MSBuild outbound subscription** | No downstream consumes MSBuild builds from that branch. |\n| Default channel exists for a **branch that isn't in the repo** | Pure orphan — delete the mapping. |\n\n**Retirement cleanup (2.3e):** remove inbound subscriptions, unused MSBuild outbound subscriptions, and branch-to-channel associations, including disabled subscriptions. Preserve shared channels and flows still needed by other branches or producers; verify removals after merge and ingestion.\n\n## Executing a Phase\n\nWhen asked to execute a specific phase:\n\n1. Read the full phase from `documentation/release-checklist.md`\n2. Verify the trigger condition is met (previous phases completed, including ingestion and live verification of prerequisite DARC changes)\n3. Execute steps in order — respect sequential/parallel annotations\n4. For DARC commands: batch writes into one configuration PR per phase\n5. Record all output URLs in the tracking issue's artifact table\n6. Mark checkboxes as completed in the tracking issue\n7. In **Phase 4**, complete telemetry checkpoints 4.0 and 4.4a using the shared evidence procedure in `documentation/release-checklist.md`.\n8. In **Phase 4** (step 4.7): if `documentation/wiki/ChangeWaves.md` is changed for this release, update the public Learn page at https://learn.microsoft.com/visualstudio/msbuild/change-waves. Sync the Change Waves Learn page from `documentation/wiki/ChangeWaves.md` on the `vsXX.Y` branch that is live in VS Insiders / the latest preview SDK. PR goes to `MicrosoftDocs/visualstudio-docs-pr` (`docs/msbuild/change-waves.md`); example: https://github.com/MicrosoftDocs/visualstudio-docs-pr/pull/15662.\n\n## Key Files\n\n| File | Purpose |\n|---|---|\n| [`documentation/release-checklist.md`](../../../documentation/release-checklist.md) | **Operational checklist** — the source of truth |\n| [`documentation/release.md`](../../../documentation/release.md) | Maintained-branch inventory and release process |\n| [`documentation/wiki/ChangeWaves.md`](../../../documentation/wiki/ChangeWaves.md) | Source doc for the Learn page sync — always sync the `vsXX.Y` (Insiders/preview-SDK) copy, not `main` |\n| [MSBuild Change Waves Learn page](https://learn.microsoft.com/visualstudio/msbuild/change-waves) | Public docs target to [`MicrosoftDocs/visualstudio-docs-pr`](https://github.com/MicrosoftDocs/visualstudio-docs-pr) (`docs/msbuild/change-waves.md`) |\n| [`eng/Versions.props`](../../../eng/Versions.props) | `VersionPrefix`, `PackageValidationBaselineVersion`, `BootstrapSdkVersion` |\n| [`azure-pipelines/vs-insertion.yml`](../../../azure-pipelines/vs-insertion.yml) | VS insertion pipeline — `TargetBranch` parameter and `InsertTargetBranch` routing |\n| [`azure-pipelines/vs-insertion-experimental.yml`](../../../azure-pipelines/vs-insertion-experimental.yml) | Experimental insertion — `TargetBranch` parameter values |\n| [`scripts/Get-PackageValidationBaseline.ps1`](../../../scripts/Get-PackageValidationBaseline.ps1) | Phase 3.2 — resolves `PackageValidationBaselineVersion` deterministically (merge-base → pipeline 9434 → dotnet-tools feed) |\n| [`scripts/Get-LatestOptProfDrop.ps1`](../../../scripts/Get-LatestOptProfDrop.ps1) | Phase 3.3 — resolves the latest main OptProf drop (MSBuild-OptProf pipeline 17389) to refresh `OptProfBaselineDrop` in `.vsts-dotnet.yml` |\n| [`.vsts-dotnet.yml`](../../../.vsts-dotnet.yml) | Build pipeline entry point — `OptProfBaselineDrop` (hardcoded OptProf seed for new `vs*` branches) |\n| [`azure-pipelines/.vsts-dotnet-build-jobs.yml`](../../../azure-pipelines/.vsts-dotnet-build-jobs.yml) | Build jobs — `VisualStudio.ChannelName` is release-agnostic (`int.main`); no per-release edit |\n\n## Validation\n\nAfter completing all phases, verify:\n\n1. Branch `vs{{THIS_RELEASE_VERSION}}` exists and has final branding\n2. Main has `VersionPrefix` = `{{NEXT_VERSION}}.0`\n3. DARC: main → `VS {{NEXT_VERSION}}` channel, release branch → `VS {{THIS_RELEASE_VERSION}}` channel\n4. VS insertion PR merged\n5. Packages published to nuget.org\n6. GitHub release created with tag `v{{THIS_RELEASE_EXACT_VERSION}}`\n7. The Learn page https://learn.microsoft.com/visualstudio/msbuild/change-waves lists exactly the waves present in `vsXX.Y` (the version in VS Insiders / the latest released preview SDK), or the sync is explicitly tracked\n\n## Error Recovery\n\n- **Branch already exists**: Release was partially started — check the tracking issue for progress\n- **DARC channel already exists**: Safe to continue — `add-channel` is idempotent\n- **OptProf fails on first build**: Expected — that's why we use main's OptProf data as fallback\n- **DARC configuration PR conflicts**: Rebase the configuration branch on `production` and force-push\n\n## Major Version Releases\n\nIf `NEXT_VERSION` is a new major version (e.g., 18.x → 19.0), additional steps are needed after Phase 5. See [release.md](../../../documentation/release.md) for:\n- `src/Shared/BuildEnvironmentHelper.cs` — VS major version constants\n- `src/Shared/Constants.cs` — version constants\n- `src/Framework/Telemetry/TelemetryConstants.cs` — telemetry version\n","frontmatter":{"name":"release","description":"Orchestrate an MSBuild release: create the tracking issue, branch, configure DARC channels and subscriptions, bump version in main, final-brand the release branch, insert into VS, and publish post-GA. Covers the full monthly release lifecycle aligned with VS shipping cadence.","argument-hint":"Specify which phase to execute (0-5) and provide required inputs (THIS_RELEASE_VERSION, NEXT_VERSION, etc.)"},"isInternal":false,"tokens":4156,"sizeBytes":17091},{"name":"SKILL.md","path":".github/skills/reviewing-msbuild-code/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/reviewing-msbuild-code/SKILL.md","title":"reviewing-msbuild-code","category":"anthropic-skill","format":"markdown","content":"---\nname: reviewing-msbuild-code\ndescription: \"Reviews MSBuild code changes using a 24-dimension methodology. Activates for code review, PR review, pull request analysis, design review, architecture review, code quality assessment, or style check of MSBuild code. Covers backwards compatibility, ChangeWave discipline, performance, allocation awareness, test coverage, error message quality, logging, string comparison, API surface, target authoring, cross-platform correctness, code simplification, concurrency, naming, SDK integration, evaluation model integrity, correctness, dependency management, security, and build infrastructure.\"\n---\n\n# MSBuild Code Review\n\nInvoke `@expert-reviewer` for 24-dimension MSBuild code review. The agent contains the full methodology, principles, dimension rules with checklists, folder hotspot mapping, and 4-wave workflow.\n","frontmatter":{"name":"reviewing-msbuild-code","description":"Reviews MSBuild code changes using a 24-dimension methodology. Activates for code review, PR review, pull request analysis, design review, architecture review, code quality assessment, or style check of MSBuild code. Covers backwards compatibility, ChangeWave discipline, performance, allocation awareness, test coverage, error message quality, logging, string comparison, API surface, target authoring, cross-platform correctness, code simplification, concurrency, naming, SDK integration, evaluation model integrity, correctness, dependency management, security, and build infrastructure."},"isInternal":false,"tokens":166,"sizeBytes":862},{"name":"SKILL.md","path":".github/skills/running-unit-tests/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/running-unit-tests/SKILL.md","title":"running-unit-tests","category":"anthropic-skill","format":"markdown","content":"---\nname: running-unit-tests\ndescription: Guide for running MSBuild unit tests efficiently. Use when running, scoping, filtering, or speeding up unit tests in this repository, or when finalizing a change with a heavier validation pass. Covers xUnit v3 + Microsoft.Testing.Platform (MTP) specifics and which `dotnet test` flags do and don't apply.\nargument-hint: Run, filter, scope, or speed up MSBuild unit tests.\n---\n\n# Running MSBuild Unit Tests\n\nThis repo uses **xUnit v3** with the **Microsoft.Testing.Platform (MTP)** runner. Test projects are built as `OutputType=Exe`, so each test assembly is a self-contained host process — *not* a classic VSTest assembly.\n\nThere are **two ways to run tests**:\n\n| Method | When to use |\n|--------|-------------|\n| `dotnet test <project>` | Dev loop — run a single test project, optionally filtered |\n| `build.cmd -test` / `build.sh --test` | Final validation — builds everything and runs all test projects via the repo's Arcade harness |\n\nUse a **fast, scoped** `dotnet test` loop while iterating, and the **full `build.cmd -test`** pass before declaring the change done.\n\n## Repo-specific knobs to know about\n\nThese are configured in `Directory.Build.props` (repo root), `src/Directory.Build.targets`, and `src/Shared/UnitTests/xunit.runner.json`. They apply to both `dotnet test` and `build.cmd -test` unless noted otherwise:\n\n- **Multi-targeting**: test projects target .NETFramework and .NETCoreApp on Windows (.NETCoreApp only on Linux/macOS). `dotnet test` runs the suite **once per TFM**.\n- **Single-threaded by default**: `xunit.runner.json` sets `maxParallelThreads: 1` and `parallelizeTestCollections: false`. Many tests mutate process-global state (env vars, cwd, SDK resolvers), so this is intentional.\n- **Auto trait filters**: platform/TFM-inappropriate tests are filtered out via `--filter-not-trait Category=...` (e.g., `nonwindowstests`, `failing`, `nonnetcoreapptests`). Don't try to \"fix\" tests that appear skipped because of these.\n- **Coverage on non-Windows**: `--coverage --coverage-settings Coverage.config` is appended unconditionally to `XunitOptions` in `src/Directory.Build.targets`. There is no MSBuild property switch to disable it from `dotnet test` — to skip coverage, run the test exe directly without `--coverage`.\n- **Test runner**: `TestRunnerName=XUnitV3`, MTP `1.9.1`, xUnit v3 `3.2.2` (set in repo-root `Directory.Build.props`).\n- **DOTNET_HOST_PATH**: `RunnerUtilities.GetMSBuildEnvironmentVariables` (in `src/UnitTests.Shared/RunnerUtilities.cs`) sets `DOTNET_HOST_PATH` to the bootstrap dotnet when tests launch MSBuild as a child process, so tasks like `RoslynCodeTaskFactory` resolve the right host. Don't override `DOTNET_HOST_PATH` from a test.\n\n## Flags — MTP vs VSTest pitfalls\n\nThis repo uses MTP with the xUnit v3 MTP runner, **not** VSTest. Many familiar `dotnet test` flags silently do nothing. Key differences:\n\n- **Use** `--report-trx` (not `--logger trx`), `--coverage` (not `--collect \"XPlat Code Coverage\"`), `--filter-method`/`--filter-class`/`--filter-trait` for xUnit v3 native filtering.\n- **Don't use** `--filter`, `--nologo`, `--blame`, `--settings *.runsettings`, `--diag`, `--collect`, or `-- RunConfiguration.MaxCpuCount=...` — these are VSTest-only and ignored.\n- **Still works**: `-c`, `-f`, `--no-restore`, `--no-build`, `-v`, `-bl:`, `-p:Property=Value` — these are interpreted by `dotnet test` itself before MTP sees them.\n\nFor comprehensive MTP vs VSTest flag reference, see the `run-tests` skill from the `dotnet-test` plugin.\n\n## Dev loop: fast and scoped\n\nAim for sub-30s iterations.\n\n> Examples below use Windows-style backslashes and PowerShell line continuations. Forward slashes work everywhere with `dotnet` (`src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj`); on Linux/macOS use `/` and shell line continuations (`\\`), and the exe path becomes `artifacts/bin/<Proj>/Debug/net11.0/<Proj>` (no `.exe`).\n\n1. **Run via `dotnet test` with a single TFM and filter** (incremental build handles rebuilding automatically):\n   ```powershell\n   dotnet test src\\Tasks.UnitTests\\Microsoft.Build.Tasks.UnitTests.csproj `\n     -f net11.0 -- --filter-method \"*MyFeature*\"\n   ```\n2. **Or run the test exe directly** (fastest — no SDK overhead; build first if source changed):\n   ```powershell\n   dotnet build src\\Tasks.UnitTests\\Microsoft.Build.Tasks.UnitTests.csproj -c Debug -f net11.0\n   artifacts\\bin\\Microsoft.Build.Tasks.UnitTests\\Debug\\net11.0\\Microsoft.Build.Tasks.UnitTests.exe `\n     --filter-method \"*MyFeature*\" --no-progress\n   ```\n   The exe path is TFM-specific: `Debug\\net11.0\\...exe` for net11.0, `Debug\\net472\\...exe` for net472. Switching TFM without rebuilding silently runs stale binaries.\n\n### Speeding up the dev loop further\n\nThese trade safety for speed — use during iteration, **revert before final validation**:\n\n-- **Single TFM**: pass `-f net11.0`. Halves runtime on Windows by skipping `net472`.\n- **Temporarily relax single-threaded execution**: drop a `xunit.runner.json` next to the test project (or override the existing one) with:\n  ```json\n  {\n    \"$schema\": \"https://xunit.net/schema/current/xunit.runner.schema.json\",\n    \"longRunningTestSeconds\": 60,\n    \"maxParallelThreads\": -1,\n    \"parallelizeTestCollections\": true\n  }\n  ```\n  Or pass them as runner args after `--`: `-- xUnit.MaxParallelThreads=-1 xUnit.ParallelizeTestCollections=true`. Expect flakes in tests that touch env vars, cwd, the file system, or the global ProjectCollection — **don't ship a fix that depends on this being on**.\n- **Skip bootstrap packaging** for non-bootstrap test projects:\n  `dotnet build ... -p:CreateBootstrap=false` (useful when the local bootstrap SDK payload is missing).\n- **Narrow with traits**: `--filter-trait Category=mytraitduringdev` if you've tagged a focused subset.\n- **Skip code coverage on Linux/macOS**: run the test exe directly without `--coverage`. This repo does not expose a supported `dotnet test` property switch to disable the auto-added coverage arguments.\n\n## Final validation pass\n\nBefore saying \"done,\" run the heavy configuration. Don't skip TFMs and don't keep parallel-overrides.\n\n1. **Restore parallelism settings** (revert any local `xunit.runner.json` change).\n2. **Run all TFMs for affected projects**:\n   ```powershell\n   dotnet test src\\Build.UnitTests\\Microsoft.Build.Engine.UnitTests.csproj -c Release\n   dotnet test src\\Tasks.UnitTests\\Microsoft.Build.Tasks.UnitTests.csproj -c Release\n   # Add other UnitTests projects whose code paths you touched\n   ```\n3. **For broad changes (engine, framework, shared)**, run the full repo test suite (~9 minutes — do not cancel):\n   ```powershell\n   .\\build.cmd -test -c Release       # Windows\n   ./build.sh --test -c Release       # Linux/macOS\n   ```\n4. **Capture a TRX** if reporting results to the user or CI:\n   ```powershell\n   dotnet test <project> -c Release `\n     --report-trx --report-trx-filename validation.trx `\n     --results-directory artifacts\\TestResults\n   ```\n\n## Reading the summary line\n\nMTP's final summary reports `passed`, `failed`, and `skipped` separately. **Always check the `skipped` count** — platform-conditional attributes (`WindowsOnlyFact`, `UnixOnlyFact`, etc.) and the auto trait filters cause expected skips, but a sudden jump in skipped count can hide a regression where a test became inapplicable on the current platform without you intending it.\n\n## Picking the right project to run\n\nMatch the source area you changed to its `*.UnitTests` project:\n\n| Source area | Test project |\n|-------------|--------------|\n| `src/Build/**` (engine, evaluation, backend) | `src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj` |\n| `src/Framework/**` | `src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj` |\n| `src/Tasks/**` | `src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj` |\n| `src/Utilities/**` | `src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj` |\n| `src/MSBuild/**` (CLI) | `src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj` |\n| `src/Build/BuildCheck/**` | `src/BuildCheck.UnitTests/Microsoft.Build.BuildCheck.UnitTests.csproj` |\n| `src/Shared/**` | Run the consumers above (Build, Tasks, Utilities) — shared code is linked into all of them. |\n\n## Quick reference\n\n| Scenario | Command |\n|----------|---------|\n| Fast scoped dev loop | `dotnet test <proj> -f net11.0 -- --filter-method \"*X*\"` |\n| Direct test exe | `artifacts\\bin\\<Proj>\\Debug\\net11.0\\<Proj>.exe --filter-method \"*X*\" --no-progress` |\n| Single test by name | `dotnet test <proj> -- --filter-method \"*MyTestMethod*\"` |\n| Final per-project validation | `dotnet test <proj> -c Release` (all TFMs) |\n| Final full validation | `.\\build.cmd -test -c Release` / `./build.sh --test -c Release` |\n| TRX report | `--report-trx --report-trx-filename out.trx --results-directory artifacts\\TestResults` |\n\n## See also\n\n- `.github/instructions/tests.instructions.md` — test authoring conventions (xUnit v3, Shouldly, `TestEnvironment`, `MockLogger`).\n- `src/Shared/UnitTests/xunit.runner.json` — repo-wide xUnit settings.\n- `src/Directory.Build.targets` — `XunitOptions`, auto trait filters, coverage wiring.\n- `documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md`\n","frontmatter":{"name":"running-unit-tests","description":"Guide for running MSBuild unit tests efficiently. Use when running, scoping, filtering, or speeding up unit tests in this repository, or when finalizing a change with a heavier validation pass. Covers xUnit v3 + Microsoft.Testing.Platform (MTP) specifics and which `dotnet test` flags do and don't apply.","argument-hint":"Run, filter, scope, or speed up MSBuild unit tests."},"isInternal":false,"tokens":2379,"sizeBytes":9307},{"name":"SKILL.md","path":".github/skills/use-bootstrap-msbuild/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/skills/use-bootstrap-msbuild/SKILL.md","title":"use-bootstrap-msbuild","category":"anthropic-skill","format":"markdown","content":"---\nname: use-bootstrap-msbuild\ndescription: Guide for testing bug reproductions against locally-built MSBuild. Use this when you have a repro project and want to verify a fix works before submitting a PR.\n---\n\n# Testing Bug Reproductions with Bootstrap MSBuild\n\nThis skill guides you through testing a bug reproduction project against your locally-built MSBuild to verify a fix.\n\n## Overview\n\nAfter making changes to MSBuild, you need to test them against a repro project. The \"bootstrap\" is a self-contained MSBuild installation built from your local changes. It includes all dependencies needed to build real projects.\n\n## Step 1: Build MSBuild with Bootstrap\n\nBuild MSBuild to create the bootstrap directory:\n\n```powershell\n# Windows\n.\\build.cmd\n\n# Unix/macOS\n./build.sh\n```\n\nThis creates the bootstrap at `artifacts\\bin\\bootstrap\\` with your changes.\n\nRerun this after making any code change. If run in the default mode, there may be some errors on subsequent builds about locked files (due to MSBuild worker node processes lingering). If so, run `./artifacts/bin/bootstrap/core/dotnet build-server shutdown`.\n\n## Step 2: Run Your Repro Project\n\n### .NET Core / .NET SDK Projects\n\nUse the bootstrap `dotnet` CLI directly (preferred):\n\n```powershell\n# Windows\nartifacts\\bin\\bootstrap\\core\\dotnet.exe build <path-to-repro.csproj>\n```\n\n```bash\n# Unix/macOS\n./artifacts/bin/bootstrap/core/dotnet build <path-to-repro.csproj>\n```\n\nAll of the usual command line arguments should work, including `-bl` to create binlogs.\n\n### .NET Framework Projects\n\nIf the problem is specific to the .NET Framework `MSBuild.exe` that is used in Visual Studio, and you're running on Windows, you can use the bootstrap MSBuild.exe directly:\n\n```powershell\nartifacts\\bin\\bootstrap\\net472\\MSBuild\\Current\\Bin\\MSBuild.exe <path-to-repro.csproj>\n```\n\n**Note**: The .NET Framework bootstrap output will only be complete when built on Windows using `MSBuild.exe`.\n\n### Changes not reflected\n\n1. Verify bootstrap was rebuilt: check `artifacts\\bin\\bootstrap\\core\\sdk\\*\\MSBuild.dll` timestamp\n2. Kill any lingering MSBuild server processes:\n   ```powershell\n   ./artifacts/bin/bootstrap/core/dotnet build-server shutdown\n   # Or use the helper function after sourcing msbuild-build-env.ps1\n   killdotnet\n   ```\n\n### Repro works with bootstrap but not with installed MSBuild\n\nYour fix is working! The repro uses your local changes while the installed MSBuild has the bug.\n\n## Quick Reference\n\n| Scenario | Command |\n|----------|---------|\n| Build bootstrap | `.\\build.cmd` |\n| .NET Core repro | `artifacts\\bin\\bootstrap\\core\\dotnet.exe build <project>` |\n| .NET Framework repro | `artifacts\\bin\\bootstrap\\net472\\MSBuild\\Current\\Bin\\MSBuild.exe <project>` |\n\n## See Also\n\n- [Bootstrap Documentation](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Bootstrap.md)\n- [Building and Debugging Guide](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md)\n","frontmatter":{"name":"use-bootstrap-msbuild","description":"Guide for testing bug reproductions against locally-built MSBuild. Use this when you have a repro project and want to verify a fix works before submitting a PR."},"isInternal":false,"tokens":709,"sizeBytes":3004},{"name":"SKILL.md","path":"plugins/mt-migration/skills/multithreaded-task-migration/SKILL.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/plugins/mt-migration/skills/multithreaded-task-migration/SKILL.md","title":"multithreaded-task-migration","category":"anthropic-skill","format":"markdown","content":"---\nname: multithreaded-task-migration\ndescription: Guide for migrating MSBuild tasks to multithreaded mode support, including compatibility red-team review. Use this when converting tasks to thread-safe versions, implementing IMultiThreadableTask, adding TaskEnvironment support, or auditing migrations for behavioral compatibility.\n---\n\n# Migrating MSBuild Tasks to Multithreaded API\n\nMSBuild's multithreaded execution model requires tasks to avoid global process state (working directory, environment variables). Thread-safe tasks declare this capability via `MSBuildMultiThreadableTask` and use `TaskEnvironment` from `IMultiThreadableTask` for safe alternatives.\n\n## Migration Steps\n\n### Step 1: Update Task Class Declaration\n\na. Ensure the task implementing class is decorated with the `MSBuildMultiThreadableTask` attribute.\nb. Implement `IMultiThreadableTask` **only if** the task needs `TaskEnvironment` APIs (path absolutization, env vars, process start). If the task has no file/environment operations (e.g., a stub class), the attribute alone is sufficient.\n\n```csharp\n[MSBuildMultiThreadableTask]\npublic class MyTask : Task, IMultiThreadableTask\n{\n    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;\n    ...\n}\n```\n\n**Note**: `[MSBuildMultiThreadableTask]` has `Inherited = false` — it must be on each concrete class, not just the base. The corollary is easy to miss: the base class still executes multithreaded, but is not analyzed. Audit the whole base chain — see [Unsafe Code in an Unannotated Base Class](#unsafe-code-in-an-unannotated-base-class).\n\n### Step 2: Absolutize Paths Before File Operations\n\nAll path strings must be absolutized with `TaskEnvironment.GetAbsolutePath()` before use in file system APIs. This resolves paths relative to the project directory, not the process working directory.\n\n```csharp\nAbsolutePath absolutePath = TaskEnvironment.GetAbsolutePath(inputPath);\nif (File.Exists(absolutePath))\n{\n    string content = File.ReadAllText(absolutePath);\n}\n```\n\nThe [`AbsolutePath`](https://github.com/dotnet/msbuild/blob/main/src/Framework/PathHelpers/AbsolutePath.cs) struct:\n- `Value` — the absolute path string\n- `OriginalValue` — preserves the input path (use for error messages and `[Output]` properties)\n- Implicitly convertible to `string` for File/Directory API compatibility\n- `GetCanonicalForm()` — resolves `..` segments and normalizes separators (see [Sin 5](#sin-5-canonicalization-mismatch))\n\n**CAUTION**: `GetAbsolutePath()` throws `ArgumentException` for null/empty inputs. See [Sin 3](#sin-3-null-coalescing-that-changes-control-flow) and [Sin 6](#sin-6-exception-type-change) for compatibility implications.\n\n### Step 3: Replace Environment Variable APIs\n\n| BEFORE (UNSAFE)                                  | AFTER (SAFE)                                       |\n|--------------------------------------------------|----------------------------------------------------|\n| `Environment.GetEnvironmentVariable(\"VAR\");`       | `TaskEnvironment.GetEnvironmentVariable(\"VAR\");`     |\n| `Environment.SetEnvironmentVariable(\"VAR\", \"v\");`  | `TaskEnvironment.SetEnvironmentVariable(\"VAR\", \"v\");` |\n\n### Step 4: Replace Process Start APIs\n\n| BEFORE (UNSAFE - inherits process state)           | AFTER (SAFE - uses task's isolated environment)     |\n|----------------------------------------------------|-----------------------------------------------------|\n| `var psi = new ProcessStartInfo(\"tool.exe\");`      | `var psi = TaskEnvironment.GetProcessStartInfo();`  |\n|                                                    | `psi.FileName = GetFullPathToTool(); // must be absolute` |\n\n## Updating Unit Tests\n\nBuilt-in MSBuild tasks now initialize `TaskEnvironment` with a `MultiProcessTaskEnvironmentDriver`-backed default. Tests creating instances of built-in tasks no longer need manual `TaskEnvironment` setup. For custom or third-party tasks that implement `IMultiThreadableTask` without a default initializer, set `TaskEnvironment = TaskEnvironment.Fallback` (or use `TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(path)` to point at a specific project directory).\n\n## APIs to Avoid\n\n| Category | APIs | Alternative |\n|---|---|---|\n| **Forbidden** | `Environment.Exit`, `FailFast`, `Process.Kill`, `ThreadPool.SetMin/MaxThreads`, `Console.*` | Return false, throw, or use `Log` |\n| **Use TaskEnvironment** | `Environment.CurrentDirectory`, `Get/SetEnvironmentVariable`, `Path.GetFullPath`, `ProcessStartInfo` | See Steps 2-4 |\n| **Need absolute paths** | `File.*`, `Directory.*`, `FileInfo`, `DirectoryInfo`, `FileStream`, `StreamReader/Writer` | Absolutize first (File System APIs) |\n| **Need absolute paths (analyzer-invisible)** | `AssemblyName.GetAssemblyName`, `XDocument/XElement/XmlDocument.Load(string)`, `XmlReader/XmlWriter.Create(string)`, `ZipFile.*`, `X509CertificateLoader`, `Image.FromFile`, `Assembly.LoadFrom` | Absolutize first — **the analyzer does not flag these** |\n| **Review required** | `Assembly.Load*`, `Activator.CreateInstance*` | Check for version conflicts |\n\n### Analyzer-Invisible Path Consumers\n\n`MSBuildTask0003` monitors a fixed list of types (`File`, `Directory`, `FileInfo`, `DirectoryInfo`, `FileStream`, `StreamReader`, `StreamWriter`, `FileSystemWatcher`). Any *other* API that accepts a path string and touches disk is equally unsafe but produces **no diagnostic**.\n\nIn a ~150-task migration across dotnet/arcade, dotnet/source-build-assets and the dotnet/dotnet VMR, `AssemblyName.GetAssemblyName` on a raw input was the single most-repeated defect. Do not treat a clean analyzer run as evidence that path handling is complete — see [Verification](#verification-a-clean-analyzer-run-is-not-a-migration).\n\nNote the overload distinction: the *string* overloads are hazards; `XDocument.Load(stream)` and `new StreamReader(stream)` are fine, because the caller already resolved the path to open the stream.\n\n## Practical Notes\n\n### CRITICAL: Trace All Path String Usage\n\nTrace every path string through all method calls and assignments to find all places it flows into file system operations — including helper methods that may internally use File System APIs.\n\n1. Find every path string (e.g., `item.ItemSpec`, function parameters)\n2. Trace downstream through all method calls\n3. Absolutize BEFORE any code path that touches the file system\n4. Use `OriginalValue` for user-facing output (logs, errors) — see [Sin 2](#sin-2-errorlog-message-path-inflation)\n\n**Trace through abstractions, not just concrete calls.** A path can reach the file system without a single `System.IO` type appearing in the task. In `InstallDotNetTool` (dotnet/arcade), the `DestinationPath`, `DotnetPath` and `WorkingDirectory` inputs flow raw through `IFileSystem` and `ICommandFactory` interfaces — the task body looks completely clean, and the analyzer reports nothing. When a task input is passed to an interface or delegate, resolve it at the task boundary rather than hoping the implementation does.\n\n**Resolve once, at the boundary.** The recurring failure is not \"forgot to absolutize\" but \"absolutized at three of four use sites\". If a variable is going to be used as a path at all, convert it to `AbsolutePath` where it enters the task and keep it that way — see [Sin 8](#sin-8-swallowed-exceptions-hiding-an-unresolved-path).\n\n### Exception Handling in Batch Operations\n\nIn batch processing (iterating over files), `GetAbsolutePath()` throwing on one bad path aborts the entire batch. Match the original task's error semantics:\n\n```csharp\nbool success = true;\nforeach (ITaskItem item in SourceFiles)\n{\n    try\n    {\n        AbsolutePath path = TaskEnvironment.GetAbsolutePath(item.ItemSpec);\n        ProcessFile(path);\n    }\n    catch (ArgumentException ex)\n    {\n        Log.LogError(\"Invalid path '{0}': {1}\", item.ItemSpec, ex.Message);\n        success = false;\n    }\n}\nreturn success;\n```\n\n### Prefer AbsolutePath Over String\n\nStay in the `AbsolutePath` world — it's implicitly convertible to `string` where needed. Avoid round-tripping through `string` and back.\n\n### TaskEnvironment is Not Thread-Safe\n\nIf your task spawns multiple threads internally, synchronize access to `TaskEnvironment`. Each task *instance* gets its own environment, so no synchronization between tasks is needed.\n\n### API Discipline When Plumbing Through Helpers\n\nWhen the migration ripples into shared helpers:\n\n- **Helper signatures: take `AbsolutePath`, not `(string path, string pathForMessages)`**. Two-string signatures drift apart; the caller passing one `AbsolutePath` keeps `.Value` and `.OriginalValue` in lockstep.\n- **Repo-wide helpers belong in an extensions class** (e.g., `TaskEnvironmentExtensions`), not on the task. If a per-task helper looks generic, extract it.\n- **Don't condition behavior on MT-mode**. `TaskEnvironment.Fallback` already gives single-process semantics; `if (mtMode) { … } else { … }` doubles the maintenance surface and skips test coverage of one branch.\n- **Don't silently swallow `ArgumentException` from `GetAbsolutePath`** in a helper — log a diagnostic so customers can debug bad inputs.\n\n## References\n\n- [Thread-Safe Tasks Spec](https://github.com/dotnet/msbuild/blob/main/documentation/specs/multithreading/thread-safe-tasks.md)\n- [`AbsolutePath`](https://github.com/dotnet/msbuild/blob/main/src/Framework/PathHelpers/AbsolutePath.cs)\n- [`TaskEnvironment`](https://github.com/dotnet/msbuild/blob/main/src/Framework/TaskEnvironment.cs)\n- [`IMultiThreadableTask`](https://github.com/dotnet/msbuild/blob/main/src/Framework/IMultiThreadableTask.cs)\n\n---\n\n# Compatibility Red-Team Playbook\n\nAfter migration, review for behavioral compatibility. **Every observable difference is a bug until proven otherwise.**\n\nObservable behavior = `Execute()` return value, `[Output]` property values, error/warning message content, exception types, files written, and which code path runs.\n\n## The 8 Deadly Compatibility Sins\n\nReal bugs found during MSBuild task migrations. Every one shipped in initial \"passing\" code with green tests.\n\n**Edge-case discipline:** For every migrated code path, verify behavior when inputs are `null`, empty string (`\"\"`), or whitespace-only. `GetAbsolutePath` throws `ArgumentException` on null/empty — if the pre-migration code handled these differently (e.g., returned early, used a default, or threw a different exception type), the migration must preserve that behavior. Even if a scenario seems unlikely, treat it as a relevant finding if it is theoretically possible.\n\n### Sin 1: Output Property Contamination\n\nAbsolutized values leak into `[Output]` properties that users/other tasks consume.\n\n```csharp\n// BROKEN: ManifestPath was \"bin\\Release\\app.manifest\", now \"C:\\repo\\bin\\Release\\app.manifest\"\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(Path.Combine(OutputDirectory, name));\nManifestPath = abs; // implicit string conversion!\n\n// CORRECT: separate original form from absolutized path\nstring originalPath = Path.Combine(OutputDirectory, name);\nAbsolutePath outputPath = TaskEnvironment.GetAbsolutePath(originalPath);\nManifestPath = originalPath;          // [Output]: original form\ndocument.Save((string)outputPath);    // file I/O: absolute path\n```\n\n**Detect**: For every `[Output]` property, trace backward — is it ever assigned from an `AbsolutePath`?\n\n### Sin 2: Error/Log Message Path Inflation\n\nError messages and exception messages show absolutized paths instead of the user's original input.\n\n**Direct leakage** — passing an `AbsolutePath` to logging APIs:\n\n```csharp\n// BROKEN: \"Cannot find 'C:\\repo\\app.manifest'\" instead of \"Cannot find 'app.manifest'\"\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(path);\nLog.LogError(\"Cannot find '{0}'\", abs); // implicit conversion!\n\n// CORRECT: use OriginalValue\nLog.LogError(\"Cannot find '{0}'\", abs.OriginalValue);\n```\n\n**Indirect leakage** — exception messages from helpers that received the absolutized path:\n\n```csharp\n// BROKEN: ex.FileName / ex.Message embed the absolutized path\ncatch (FileNotFoundException ex) { Log.LogError(\"Not found: {0}\", ex.FileName); }\ncatch (Exception ex)              { Log.LogError(ex.Message); }\n\n// CORRECT: prefer the original input; if you must use the exception, sanitize\ncatch (FileNotFoundException ex) { Log.LogError(\"Not found: {0}\", abs.OriginalValue); }\ncatch (Exception ex)              { Log.LogError(ex.Message.Replace(abs.Value, abs.OriginalValue)); }\n```\n\n**Detect**: Search every `Log.LogError`/`LogWarning`/`LogMessage` — is any argument an `AbsolutePath`? Also check every `Log.LogError(ex.Message …)` / `ex.FileName` downstream of a `GetAbsolutePath` — did the exception originate from a helper that received the absolutized path?\n\n### Sin 3: Null Coalescing That Changes Control Flow\n\nAdding `?? \"\"` silently swallows an exception the old code relied on for error handling.\n\n```csharp\n// BEFORE: Path.GetDirectoryName(\"C:\\\") → null → Path.Combine(null, x) → ArgumentNullException\n//   → task fails with an exception / error logged → Execute() returns false\n\n// BROKEN: ?? \"\" added \"for safety\"\nstring dir = Path.GetDirectoryName(fileName) ?? string.Empty;\n// Path.Combine(\"\", x) succeeds silently → no error → Execute() returns TRUE!\n```\n\n**Detect**: For every `??` you added, ask: \"What happened when this was null before?\" If it threw and was caught → your `??` is a bug.\n\n### Sin 4: Try-Catch Scope Mismatch\n\n`GetAbsolutePath()` inside a try block leaves the absolutized value out of scope in the catch block. Helper methods in the catch (like `LockCheck`) then use the original non-absolute path.\n\n```csharp\n// CORRECT: hoist above try so catch can use it too\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(OutputManifest.ItemSpec);\ntry {\n    WriteFile(abs);\n} catch (Exception ex) {\n    string lockMsg = LockCheck.GetLockedFileMessage(abs);        // absolute → correct file\n    Log.LogError(\"Failed: {0}\", OutputManifest.ItemSpec, ...);   // original → user-friendly\n}\n```\n\n**Detect**: For every `GetAbsolutePath` inside a try, check if the catch block needs the absolutized value.\n\n### Sin 5: Canonicalization Mismatch\n\n`GetAbsolutePath` does NOT canonicalize. `Path.GetFullPath` does TWO things: absolutize AND canonicalize (`..` resolution, separator normalization). If the old code used `Path.GetFullPath` for dictionary keys, comparisons, or display, you must add `.GetCanonicalForm()`:\n\n```csharp\n// GetAbsolutePath(\"foo/../bar\")  → \"C:\\repo\\foo/../bar\"  (NOT canonical)\n// Path.GetFullPath(\"foo/../bar\") → \"C:\\repo\\bar\"         (canonical)\n\n// BROKEN for dictionary keys — \"C:\\repo\\foo\\..\\bar\" ≠ \"C:\\repo\\bar\"\nvar map = items.ToDictionary(p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec), ...);\n\n// CORRECT\nvar map = items.ToDictionary(\n    p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec).GetCanonicalForm(),\n    StringComparer.OrdinalIgnoreCase);\n```\n\n**Detect**: Find every `Dictionary`/`HashSet`/`ToDictionary` using path keys, and every place the old code called `Path.GetFullPath`. If canonicalization mattered, add `.GetCanonicalForm()`.\n\n### Sin 6: Exception Type Change\n\nOld code threw `FileNotFoundException` for missing files; new code throws `ArgumentException` from `GetAbsolutePath(\"\")` before reaching the file check. Custom catch blocks filtering by exception type may be bypassed. (`ExceptionHandling.IsIoRelatedException` catches `ArgumentException`, but task-specific handlers might not.)\n\n**Detect**: For every `GetAbsolutePath`, check what the old code threw for null/empty and whether the calling code has type-specific catch blocks.\n\n### Sin 7: `Path.IsPathRooted` as an Absolutize Gate\n\n`Path.IsPathRooted` returns `true` for **drive-relative** (`C:foo\\bar`) and **root-relative** (`\\foo\\bar`) Windows paths — both still depend on process current-directory / current-drive state. \"Absolutize only if not rooted\" silently leaves those paths process-dependent.\n\n`GetAbsolutePath` correctly handles all path forms — including the Windows edge cases (`C:foo`, `\\foo`) that `Path.IsPathRooted` considers \"rooted\" but that are still CWD/drive-dependent. Call it unconditionally; remove any `IsPathRooted` short-circuit.\n\n### Sin 8: Swallowed Exceptions Hiding an Unresolved Path\n\nThe usual mental model is \"unresolved path → `FileNotFoundException` → loud failure\". That model is wrong wherever a `catch` treats an exception as a *semantic answer*. There, an unresolved path does not fail the build — it silently returns the wrong answer.\n\nReal example from the dotnet/dotnet VMR (`CheckForPoison`), where the same variable is resolved three lines later:\n\n```csharp\n// BROKEN\ntry\n{\n    AssemblyName asm = AssemblyName.GetAssemblyName(fileToCheck);   // raw, relative-capable\n    ...\n    if (IsAssemblyFromSbrp(TaskEnvironment.GetAbsolutePath(fileToCheck))) { ... }   // resolved\n    else if (IsAssemblyPoisoned(TaskEnvironment.GetAbsolutePath(fileToCheck))) { ... }\n}\ncatch (BadImageFormatException) { /* not a managed assembly - fine */ }\n```\n\nThe `GetAssemblyName` call is only an \"is this a managed assembly?\" probe, and the catch means \"not an assembly\". So an unresolved path throws, gets swallowed, and **both poison checks are skipped** — turning a leaked binary into a false negative in the leak-detection gate itself. The build stays green while the check it exists to perform quietly stops working.\n\nThis sin is especially dangerous because it defeats the usual verification strategy: the decoy-CWD test (Pattern A) still \"passes\" unless it asserts on the *result*, not just on `Execute()` returning true.\n\n**Detect**: For every `catch` in a migrated task, ask what the handler *means*. If it means anything other than \"fail\" — \"not an assembly\", \"not found, use default\", \"unsupported, skip\" — then every path-consuming call inside the corresponding `try` must be absolutized, and the catch should be narrowed to the exception types that genuinely represent that semantic answer (`BadImageFormatException` here, **not** `IOException`/`ArgumentException`).\n\n---\n\n## Call-Chain Hazards Beyond the Task\n\nThe 7 sins above are *local* to the task body. The other half of MT migration is auditing the **transitive call chain** — helpers and shared utility classes the task reaches into.\n\n### Helpers That Capture Process State\n\nHelpers reached from `Execute()` can quietly depend on process state in any of these ways:\n\n- A relative path falls through to `Directory.GetCurrentDirectory()` (often as a \"fallback\") or to `Path.GetFullPath(x)` without a base.\n- An env var is read directly via `Environment.GetEnvironmentVariable` (not `TaskEnvironment`).\n- A `static` field is seeded from process state on first use (`static string s_x = Directory.GetCurrentDirectory()`), permanently capturing the first caller's environment for every later caller.\n- A BCL API echoes its input path back through `ex.Message` / `ex.FileName` (Sin 2).\n- The helper takes a `string path` and returns a `string` — losing the `.OriginalValue` distinction, so callers either lie in messages or absolutize twice.\n\n**Resolution patterns:**\n\n1. Add an MT-aware overload that takes `TaskEnvironment` (or an `AbsolutePath` / explicit fallback path). Legacy overload delegates with `TaskEnvironment.Fallback`. Don't branch on \"MT mode on/off\".\n2. Promote the parameter type to `AbsolutePath` so `.Value` and `.OriginalValue` travel together.\n3. Replace process-state-seeded static caches with `ConcurrentDictionary<TKey, TValue>` keyed on the inputs that determine uniqueness — never on process state.\n4. For cross-repo helpers (foundation types used by many tasks), migrate the foundation in a dedicated PR before the tasks that consume it; the hydration step is usually where `Path.GetFullPath` is hiding.\n\n### Unsafe Code in an Unannotated Base Class\n\n`[MSBuildMultiThreadableTask]` is `Inherited = false`, so it goes on each concrete task. The consequence people miss is the mirror image: **the base class runs multithreaded too, but nothing marks it as such** — and with `msbuild_task_analyzer.scope = multithreadable_only` (the recommended setting for incremental migration) the analyzer does not look inside it at all.\n\nFrom dotnet/arcade: `CreateAkaMSLinks` and `DeleteAkaMSLinks` were both annotated, both analyzer-clean. Their shared base contained:\n\n```csharp\n// AkaMSLinksBase - NOT annotated, so never analyzed\nFile.ReadAllText(ClientCertificate)   // ClientCertificate is a task input property\n```\n\nA monitored API (`File`) on a raw task input — exactly what the analyzer exists to catch — invisible purely because of where it lived.\n\n**Resolution:** when you annotate a task, audit its full base chain up to `Task`/`ToolTask`. If the base holds shared input properties or path handling, have the **base** implement `IMultiThreadableTask` and do the resolution there, so every derived task inherits the fix:\n\n```csharp\npublic abstract class AkaMSLinksBase : Task, IMultiThreadableTask\n{\n    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;\n    // ... resolve ClientCertificate here, once, for all derived tasks\n}\n```\n\nNote that implementing `IMultiThreadableTask` on the base is safe and does not change routing — routing keys off the attribute, which stays on the concrete classes.\n\n### Engine-Owned Shared State: `RegisterTaskObject`\n\nThe guidance about `static` fields has a less obvious sibling: `IBuildEngine4.GetRegisteredTaskObject` / `RegisterTaskObject`. Under MT, in-process thread nodes share this engine-owned state. Tasks in different worker or TaskHost processes do not.\n\nUsing these APIs is a **review signal, not a warning**. Check object thread safety, key collisions, and whether correctness depends on two task invocations sharing a cache.\n\nThe get/register sequence is not atomic. Two tasks can both miss and create an object. Only one equal-key object is retained; `RegisterTaskObject` returns no result, and MSBuild does not dispose an unretained object. Whether that matters depends on what is cached:\n\n| Cached computation | Review result |\n|---|---|\n| Pure and deterministic for the key | Safe if the losing object needs no cleanup, or the task cleans it up |\n| Deduplication is the point, such as logging once per build | Unsafe because both tasks can perform the side effect |\n| A failure | Check that the key includes every input that affects the result; otherwise do not cache it |\n\nReal examples from dotnet/arcade: `LocateDotNet` was safe because its computation was pure. `SingleError` was not safe because its purpose was to emit exactly one error per build, so its annotation was removed.\n\n### Tasks Instantiated Directly by Other Tasks\n\n`[MSBuildMultiThreadableTask]` is only honored when the TaskFactory creates the task (i.e., a target invokes it as `<MyTask … />`). A task created via `new MyTask()` inside another task's body bypasses the factory and **never gets `TaskEnvironment` injected** — it silently defaults to `TaskEnvironment.Fallback` (= process CWD).\n\nMigrating a nested task in isolation is therefore meaningless. Either migrate the parent and have it explicitly propagate its `TaskEnvironment` to the nested instance before calling `Execute()`, or restructure so the nested task is a regular TaskFactory-created task.\n\n**Detect:** grep for `new …Task()` followed by `.Execute()` — every direct instantiation is a hole.\n\n### ToolTask Override Hot Spots\n\n`ToolTask` subclasses inherit virtual methods that run *before or after* `Execute()` and frequently touch the file system. Audit every override:\n\n| Override | Hazard | Migration |\n|---|---|---|\n| `GenerateFullPathToTool()` | Builds `ProcessStartInfo.FileName` — relative tool path → wrong tool launched | Absolutize the tool path before returning |\n| `SkipTaskExecution()` | Up-to-date check on input/output timestamps using relative paths | Absolutize both sides of the comparison |\n| `ValidateParameters()` | `File.Exists` on input parameters | Absolutize before probing |\n| `GenerateResponseFileCommands()` / `GenerateCommandLineCommands()` | Tempting to absolutize args | **Don't** — the child's `WorkingDirectory` is the project dir, so relative args resolve correctly; absolutizing inflates user-visible output and can leak into tool diagnostics or generated artifacts |\n| `GetWorkingDirectory()` | Default `null` → child inherits host CWD | Leave alone if you use `TaskEnvironment.GetProcessStartInfo()`; it already sets `WorkingDirectory` |\n\n**Key contract:** `ProcessStartInfo.FileName` is resolved by the OS **before** `WorkingDirectory` takes effect, so the executable path must be absolute. Tool arguments are interpreted by the child process in its working directory, so they should stay relative.\n\n### Engine-Owned Environment Variables Are Immutable in MT Mode\n\nEnv vars consumed by the engine *before* tasks run (e.g., `MSBUILD*`-prefixed flags and the framework/SDK discovery variables) are snapshotted into engine caches at startup. Tasks that mutate them later have no observable effect on the engine — but the mutation appears to succeed locally, masking bugs.\n\nIf a migrated task previously mutated such a variable to influence a downstream tool, re-architect to pass the value via `ProcessStartInfo.EnvironmentVariables` on the child process instead of mutating the parent's environment.\n\n---\n\n## Test Patterns for MT Migrations\n\nA migration test must **fail when the migration is undone**. Tests that pass identically against pre- and post-migration code are theater. The two patterns below are the ones that reliably exercise MT-specific behavior.\n\n### Pattern A: Decoy-CWD Test (for tasks that touch the file system)\n\nSet the process working directory to a **decoy** dir with no relevant files; set `TaskEnvironment.ProjectDirectory` to a different dir with the inputs/expected outputs. The task must read/write against the project directory, not the decoy.\n\n```csharp\nusing TestEnvironment env = TestEnvironment.Create();\nTransientTestFolder projectDir = env.CreateFolder();\nTransientTestFolder decoyCwd = env.CreateFolder();\nFile.WriteAllText(Path.Combine(projectDir.Path, \"input.txt\"), \"expected\");\n\nenv.SetCurrentDirectory(decoyCwd.Path); // auto-restored when env is disposed\n\nvar task = new MyTask\n{\n    TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir.Path),\n    Input = \"input.txt\", // relative\n};\ntask.Execute().ShouldBeTrue();\n```\n\n**CRITICAL:** `Directory.SetCurrentDirectory` is **process-global**. xUnit parallelizes tests within a class by default — a CWD-mutating test will flake unless pinned to a non-parallel collection (`[Collection]` + `[CollectionDefinition(DisableParallelization = true)]`). The `IDisposable` restore protects sequential safety but not concurrent siblings.\n\n### Pattern B: Cross-Instance Independence (for tasks doing relative-path resolution)\n\nTwo task instances, two `TaskEnvironment.ProjectDirectory` values, same relative input. Assert each task's output is rooted to its own project directory — proving resolution is per-instance, not shared.\n\n### When NOT to Write a \"Migration Test\"\n\nAttribute-only migrations (just `[MSBuildMultiThreadableTask]`, no `IMultiThreadableTask`) on tasks whose `Execute()` body contains no file/env/process/static-state interactions have nothing to test — a decoy-CWD test would pass whether the attribute is present or not. **Don't write the test.** Document the call-chain audit conclusion in the PR description instead.\n\n### Test Hygiene\n\n- Use `TestEnvironment` / `TransientTestFolder` for temp dirs; never `Path.GetTempPath() + Guid.NewGuid()` with manual `try/finally`.\n- Hard-code expected assertion values; don't recompute them from `Path.Combine(...)` in the assertion — when the test fails you want to know what was actually wrong.\n\n---\n\n## Verification: A Clean Analyzer Run Is Not a Migration\n\n`Microsoft.Build.TaskAuthoring.Analyzer` is the right first line of defense, and it should be enabled. It is not sufficient on its own, and it is worth being concrete about why so you know what work it leaves you.\n\n**The data.** In a migration of ~150 tasks across dotnet/arcade, dotnet/source-build-assets and the dotnet/dotnet VMR, the analyzer was enabled in all three repos and every build was **0 warnings, 0 errors**. A manual audit afterwards found **9 real defects**:\n\n| Why the analyzer missed it | Count | Example |\n|---|---|---|\n| The code was outside the analysis scope | 2 | unsafe call in an unannotated base class |\n| The API is not on the monitored list | 3 | `AssemblyName.GetAssemblyName` |\n| The failure class is not modeled at all | 4 | task-object race, memoized failure, nested task construction, path crossing a DI boundary |\n\nThe last row is the important one: those are dataflow and lifetime problems, not banned-API problems. No amount of allowlist tuning reaches them, which is why the [Red-Team Audit Protocol](#red-team-audit-protocol) below is not optional.\n\n**Why \"it worked when I tested it\" is weak evidence.** An unresolved relative path in MT mode does not reliably throw. It resolves against whatever the shared node's current directory happens to be, which depends on which project scheduled first. So the same defect passes locally single-threaded every time, passes in CI most of the time, and fails occasionally on one machine under load. And per [Sin 8](#sin-8-swallowed-exceptions-hiding-an-unresolved-path), some never throw at all.\n\n**Verification checklist.** The [Sign-Off Checklist](#sign-off-checklist) at the end of this document is the actionable list. The items most likely to be skipped — because nothing warns you about them — are:\n\n1. The full base chain, not just the concrete class.\n2. Every `catch`, classified: does it mean \"fail\", or a semantic answer? (Sin 8)\n3. Every task input traced to *every* use site — the common defect is resolved at 3 of 4 sites, not 0 of 4.\n4. Caches and memoization: `static` fields, `RegisterTaskObject`, and anything storing a failure.\n5. A Pattern A or Pattern B test that fails when the migration is reverted — asserting on the *result*, not merely that `Execute()` returned true.\n\nIf your host supports agents, the `mt-migration-reviewer` agent automates most of this.\n\n## Migrating Is Not Always the Right Answer\n\nAdding the attribute is a claim that the task is safe to run concurrently in a shared process. When that claim cannot be made honestly, **leaving the task unannotated is a correct, supported outcome** — it simply keeps routing to the TaskHost sidecar, which is exactly the behavior it has today. A slower task is better than a wrong one.\n\nDe-annotate (or never annotate) when:\n\n- The task's semantics depend on being a singleton within the build — `SingleError` above is the canonical case.\n- The task deliberately mutates process-global state for a downstream consumer and cannot be re-architected to pass it via `ProcessStartInfo`.\n- The correctness argument depends on a race being benign and you cannot convince yourself it is.\n\nWhen you do this, leave a comment saying *why*, so the next person does not \"fix\" the missing attribute:\n\n```csharp\n// Deliberately not [MSBuildMultiThreadableTask]: this task must report exactly\n// one error per build, and the Get/RegisterTaskObject pair used to enforce that\n// is not atomic across concurrent instances in a shared node.\n```\n\n---\n\n## Red-Team Audit Protocol\n\n### Phase 1: Trace Every Changed Line\n\nFor each modified line: What was the exact runtime value before? After? Where does it flow (outputs, logs, file paths, dictionary keys)? Does each destination produce identical observable behavior?\n\n### Phase 2: Null/Empty/Edge Input Analysis\n\n| Input | `GetAbsolutePath` | Old behavior | Match? |\n|---|---|---|---|\n| `null` | `ArgumentException` | Varies | ❓ |\n| `\"\"` | `ArgumentException` | Varies | ❓ |\n| `\"C:\\\"` (root) | Valid | Valid | ✅ usually |\n| `\".\"` | `\"C:\\repo\\.\"` (not canonical) | `\"C:\\repo\"` if GetFullPath | ❌ maybe |\n| `\"foo\\..\\bar\"` | `\"C:\\repo\\foo\\..\\bar\"` | `\"C:\\repo\\bar\"` if GetFullPath | ❌ maybe |\n| Already absolute | Pass-through | Pass-through | ✅ |\n\n`Path.GetDirectoryName` → `Path.Combine` chains:\n| Input | `GetDirectoryName` returns | `Path.Combine(result, x)` |\n|---|---|---|\n| `\"C:\\\"` | `null` | Throws `ArgumentNullException` |\n| `\"\"` | `\"\"` (.NET Fx) / `null` (.NET Core+) | Works / Throws ⚠️ |\n| `\"file.resx\"` (no dir) | `\"\"` | Works |\n\nVerify behavior on **both** .NET Framework and .NET TFMs.\n\n### Phase 3: Downstream Impact\n\n1. **Output properties**: What consumes this task's `[Output]`? Does it compare, display, or use as a path?\n2. **Written files**: Does file content change? (e.g., XML with embedded paths)\n3. **Helper methods**: Do `LockCheck`, `ManifestWriter`, etc. internally resolve relative paths?\n4. **Error codes**: MSBuild error codes (MSBxxxx) must be identical.\n\n### Phase 4: Concurrency\n\n1. Two tasks with different `ProjectDirectory` values don't interfere\n2. No writes to static fields (shared across threads)\n3. Every registered task object is reviewed for thread safety, key collisions, registration races, and process-local visibility\n4. No cached *failures* — a failure computed under one task's environment must not be served to another\n5. All file operations use absolutized paths\n\n## Compatibility Test Matrix\n\n```\n[Task] × [Input Type] × [Assertion]\n\nInputs: relative path, absolute path, null, empty, \"..\" segments, root \"C:\\\",\n        forward slashes, trailing separator, UNC path, 260+ char path\n\nAssertions: Execute() return value, [Output] exact string, error message content,\n            exception type, file location, file content\n```\n\n## Sign-Off Checklist\n\n- [ ] `[MSBuildMultiThreadableTask]` on every concrete class (not just base — `Inherited=false`)\n- [ ] **Base chain audited** up to `Task`/`ToolTask` — unannotated bases still run multithreaded and are *not* analyzed\n- [ ] `IMultiThreadableTask` on classes that use `TaskEnvironment` APIs, with default initializer `= TaskEnvironment.Fallback`\n- [ ] Every `[Output]` property: exact string value matches pre-migration (Sin 1)\n- [ ] Every `Log.LogError`/`LogWarning`: path in message matches pre-migration (use `OriginalValue`) (Sin 2)\n- [ ] Every `Log.LogError(ex.Message …)` / `ex.FileName`: exception path sanitized to original input (Sin 2)\n- [ ] Every `GetAbsolutePath` call: null/empty exception behavior matches old code path (Sin 3, 6)\n- [ ] Every dictionary/set with path keys: canonicalization preserved (`GetCanonicalForm()`) (Sin 5)\n- [ ] Every try-catch: absolutized value available in catch block where needed (Sin 4)\n- [ ] Every `??` or `?.` added: verified it doesn't swallow a previously-thrown exception (Sin 3)\n- [ ] No `Path.IsPathRooted` short-circuits around `GetAbsolutePath` — call unconditionally (Sin 7)\n- [ ] Every `catch` classified — any handler meaning a *semantic answer* rather than \"fail\" has all path calls in its `try` absolutized, and is narrowed to the types representing that answer (Sin 8)\n- [ ] **Analyzer-invisible path consumers** absolutized: `AssemblyName.GetAssemblyName`, `XDocument`/`XmlDocument.Load(string)`, `XmlReader.Create(string)`, `ZipFile.*`, `X509CertificateLoader`, `Assembly.LoadFrom` — none of these produce a diagnostic\n- [ ] Every task input resolved at **every** use site, not most of them\n- [ ] Task inputs crossing an interface / delegate / DI boundary (`IFileSystem`, `ICommandFactory`) resolved at the task boundary\n- [ ] No `AbsolutePath` leaks into user-visible strings unintentionally\n- [ ] No behavior conditioned on \"MT mode on/off\" — `TaskEnvironment.Fallback` handles single-process case\n- [ ] **Call chain traced end-to-end** for every helper invoked from `Execute()`:\n    - No `Environment.CurrentDirectory` / `Directory.GetCurrentDirectory()` / `Path.GetFullPath(x)` without a base anywhere in the transitive call graph\n    - No direct `Environment.Get/SetEnvironmentVariable` (route through `TaskEnvironment`)\n    - No `static` mutable fields seeded from process state; replace with `ConcurrentDictionary` keyed on inputs\n    - Every `RegisterTaskObject` use reviewed for object thread safety, key collisions, registration races, and process-local visibility\n    - No cached *failures* whose key omits inputs that affect the result\n    - No `Console.*`, `Environment.Exit`, `Process.Kill`, `FailFast`\n- [ ] ToolTask overrides audited (`GenerateFullPathToTool`, `SkipTaskExecution`, `ValidateParameters`); `ProcessStartInfo.FileName` is absolute, tool *arguments* stay relative\n- [ ] No nested tasks created via `new …Task()` without explicit `TaskEnvironment` propagation\n- [ ] No mutations of engine-owned env vars (e.g. `MSBUILD*` and the framework/SDK discovery vars) in MT mode\n- [ ] Tests for custom tasks set `TaskEnvironment = TaskEnvironmentHelper.CreateForTest()` (built-in tasks have a default)\n- [ ] Migration test follows Pattern A (decoy-CWD) **or** Pattern B (cross-instance independence), or PR explains why no test is meaningful\n- [ ] CWD-mutating tests pinned to a non-parallel xUnit collection\n- [ ] Cross-framework: tested on .NET Framework and .NET TFMs.\n- [ ] If the task was **deliberately left unannotated**, a comment records why (see [Migrating Is Not Always the Right Answer](#migrating-is-not-always-the-right-answer))\n","frontmatter":{"name":"multithreaded-task-migration","description":"Guide for migrating MSBuild tasks to multithreaded mode support, including compatibility red-team review. Use this when converting tasks to thread-safe versions, implementing IMultiThreadableTask, adding TaskEnvironment support, or auditing migrations for behavioral compatibility."},"isInternal":false,"tokens":8452,"sizeBytes":37399},{"name":"marketplace.json","path":".github/plugin/marketplace.json","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/.github/plugin/marketplace.json","title":"msbuild-plugins","category":"marketplace","format":"json","content":"{\n  \"name\": \"msbuild-plugins\",\n  \"owner\": {\n    \"name\": \"dotnet/msbuild\"\n  },\n  \"metadata\": {\n    \"description\": \"GitHub Copilot plugins for the MSBuild ecosystem\",\n    \"version\": \"1.0.0\"\n  },\n  \"plugins\": [\n    {\n      \"name\": \"mt-migration\",\n      \"description\": \"MSBuild multithreaded task migration toolkit: the migration playbook + an MT-specific code reviewer that traces call chains end-to-end.\",\n      \"version\": \"0.1.0\",\n      \"source\": \"./plugins/mt-migration\"\n    }\n  ]\n}\n","frontmatter":{"name":"msbuild-plugins","owner":{"name":"dotnet/msbuild"},"metadata":{"description":"GitHub Copilot plugins for the MSBuild ecosystem","version":"1.0.0"},"plugins":[{"name":"mt-migration","description":"MSBuild multithreaded task migration toolkit: the migration playbook + an MT-specific code reviewer that traces call chains end-to-end.","version":"0.1.0","source":"./plugins/mt-migration"}]},"isInternal":false,"tokens":135,"sizeBytes":483},{"name":"plugin.json","path":"plugins/mt-migration/plugin.json","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/plugins/mt-migration/plugin.json","title":"mt-migration","category":"plugin-manifest","format":"json","content":"{\n  \"name\": \"mt-migration\",\n  \"version\": \"0.1.0\",\n  \"description\": \"MSBuild multithreaded task migration toolkit: the migration playbook + an MT-specific code reviewer that traces call chains end-to-end. Portable across repos that consume Microsoft.Build.Framework's TaskEnvironment/IMultiThreadableTask API (e.g., dotnet/msbuild, dotnet/sdk, and any task assembly migrating to the MT model).\",\n  \"skills\": [\"./skills/\"],\n  \"agents\": [\n    \"./agents/mt-migration-reviewer.agent.md\"\n  ]\n}\n","frontmatter":{"name":"mt-migration","version":"0.1.0","description":"MSBuild multithreaded task migration toolkit: the migration playbook + an MT-specific code reviewer that traces call chains end-to-end. Portable across repos that consume Microsoft.Build.Framework's TaskEnvironment/IMultiThreadableTask API (e.g., dotnet/msbuild, dotnet/sdk, and any task assembly migrating to the MT model).","skills":["./skills/"],"agents":["./agents/mt-migration-reviewer.agent.md"]},"isInternal":false,"tokens":124,"sizeBytes":488},{"name":"README.md","path":"plugins/mt-migration/README.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/plugins/mt-migration/README.md","title":"mt-migration Documentation","category":"plugin-manifest","format":"markdown","content":"# mt-migration plugin\n\nA GitHub Copilot plugin that bundles the MSBuild **multithreaded task migration** playbook plus an MT-specific PR reviewer.\n\nUse this on any repository that authors MSBuild tasks consuming the\n`IMultiThreadableTask` / `TaskEnvironment` / `[MSBuildMultiThreadableTask]` API\nsurface from `Microsoft.Build.Framework` — currently `dotnet/msbuild`,\n`dotnet/sdk`, and downstream task assemblies (NuGet, EF, ASP.NET Core, etc.).\n\n## Contents\n\n```\nmt-migration/\n├── plugin.json\n├── skills/\n│   └── multithreaded-task-migration/\n│       └── SKILL.md         # The 8 deadly sins, ToolTask hazards, helper patterns, test patterns, sign-off checklist\n└── agents/\n    └── mt-migration-reviewer.agent.md   # MT-specific PR reviewer that follows call chains end-to-end\n```\n\n## What it does\n\n| Component | When invoked | What it does |\n|---|---|---|\n| `multithreaded-task-migration` skill | Author migrating a task | Step-by-step migration recipe + compatibility red-team checklist. Distilled from ~50 merged migration PRs across `dotnet/msbuild` and `dotnet/sdk`. |\n| `mt-migration-reviewer` agent | Reviewing an MT migration PR | Delegates the 24-dimension general review to the host repo's expert reviewer (if any), then layers MT-specific findings on top. Mandatorily traces every call chain from `Execute()` to leaves and reports hazards by chain, not by file. Verifies tests are not theater. |\n\nThe reviewer is deliberately complementary to a generic code reviewer: it does\nnot re-explain migration steps, does not redo style/perf/naming review, and\nwill not flag a clean attribute-only migration as needing a concurrency test if\nthe call-chain audit comes back clean.\n\n## Installing\n\n### As a local plugin (from a clone of this repo)\n\n```sh\n# from a GitHub Copilot session\n/plugin install file://$(pwd)/plugins/mt-migration\n```\n\n### Across multiple repos (recommended)\n\nPublish this plugin folder to a small standalone repo (e.g.\n`your-org/mt-migration-plugin`) and install from there. The plugin contains no\nhard-coded references to `dotnet/msbuild` repo-local filesystem paths — only to\nthe public `Microsoft.Build.Framework` API surface (with source links for\nreference) — so it works on any task-authoring repo unchanged.\n\n```sh\n/plugin install github:your-org/mt-migration-plugin\n```\n\n## Relationship to the in-repo skill\n\nThis plugin is the **single, canonical** home of the MT migration skill. It is not duplicated under `.github/skills/` in `dotnet/msbuild` — install the plugin to use it locally. The skill-validator workflow in `dotnet/msbuild` only scans `.github/skills/` and `.github/agents/`, so the plugin lives outside that scope and is not subject to it.\n\n## Updating the skill from PR experience\n\nThe skill's \"deadly sins\" and call-chain hazard table grow as new\ndefect classes are found in merged migrations. To contribute:\n\n1. Find a defect that was caught in PR review and would have been missed by\n   the existing skill.\n2. Add a sin / hazard / pattern with a real-world citation (PR + line range).\n3. Update `skills/multithreaded-task-migration/SKILL.md`.\n4. Keep the agent file slim — it should keep delegating to the skill rather\n   than absorbing the rules itself.\n","isInternal":false,"tokens":765,"sizeBytes":3255},{"name":"README.md","path":"src/StringTools/README.md","rawUrl":"https://raw.githubusercontent.com/dotnet/msbuild/HEAD/src/StringTools/README.md","title":"StringTools Documentation","category":"plugin-manifest","format":"markdown","content":"# Microsoft.NET.StringTools\n\nThis package contains the Microsoft.NET.StringTools assembly which implements common string-related functionality such as weak interning.\n\nAt this time, this is primarily an internal implementation detail of MSBuild and Visual Studio and we do not expect other consumers of the package. If you think you might like to use it, please start a discussion at https://github.com/dotnet/msbuild/discussions to let us know your use cases.\n","isInternal":false,"tokens":89,"sizeBytes":461}],"systemPromptSnippet":"<yakaai_skills repo=\"dotnet/msbuild\">\n<!-- File: AGENTS.md (Tokens: ~2334 | Category: root-instruction) -->\n# Agent Instructions\n\nInstructions for GitHub Copilot and other AI coding agents working with the MSBuild repository.\n\n## Repository Overview\n\n**MSBuild** is the Microsoft Build Engine - performance-critical infrastructure for .NET and Visual Studio builds. This repository contains the code for the MSBuild build engine, including its public C# API, its internal implementation of the MSBuild programming language, and core targets and tasks used for builds.\n\n### Key Components\n- **Microsoft.Build**: Core MSBuild engine and public API\n- **Microsoft.Build.Framework**: Framework interfaces and base types\n- **Microsoft.Build.Tasks**: Built-in MSBuild tasks\n- **Microsoft.Build.Utilities**: Utility classes for task authors\n- **MSBuild CLI**: Command-line tool for invoking builds\n\n### Technology Stack\n- .NET 10.0 and .NET Framework 4.7.2\n- C# 14 features (especially collection expressions)\n- xUnit with Shouldly for testing\n- Multi-platform support (Windows, Linux, macOS)\n\n## General\n\n* Performance is the top priority - minimize allocations, avoid LINQ in hot paths, use efficient algorithms.\n* Always use the latest C# features, currently C# 14, especially collection expressions (`[]` over `new Type[]`).\n* Match the style of surrounding code when making edits, but modernize aggressively for substantial changes.\n\n## Code Review Instructions\n\nOfficial builds treat all warnings as errors, so do not introduce new warnings.\n\n### Performance Considerations\n\nWhen reviewing pull requests:\n\n* **Flag any unnecessary allocations** in hot paths\n* **Flag LINQ usage** in performance-critical code paths\n* **Check for proper use of `Span<T>`** and `ReadOnlySpan<T>` for string parsing\n* **Ensure immutable collections** use the correct type (`ImmutableArray<T>` and `FrozenDictionary<TKey, TValue>` for read-heavy, `ImmutableList<T>` for incremental building)\n\n### NuGet Feed Configuration\n\nWhen reviewing pull requests:\n\n* **Flag any changes to NuGet.config** that add external package sources without justification\n* Package sources should use approved internal feeds when possible\n\n## Formatting\n\n* Apply code-formatting style defined in `.editorconfig`.\n* Prefer file-scoped namespace declarations and single-line using directives.\n* Insert a newline before the opening curly brace of any code block.\n* Use pattern matching and switch expressions wherever possible.\n* Use `nameof` instead of string literals when referring to member names.\n\n### Nullable Reference Types\n\n* **New files**: Always use nullable reference types (do NOT add `#nullable disable`)\n* **Existing files with `#nullable disable`**: Match the existing style; don't add nullable annotations (`?`) to types\n* **Existing files with nullable enabled**: Use proper nullable annotations\n* Always use `is null` or `is not null` instead of `== null` or `!= null`\n\n## Performance Best Practices\n\n### Range Pattern Matching\n\n```csharp\n// GOOD: Clear and efficient\nreturn errorNumber switch\n{\n    >= 3001 and <= 3999 => Category.Tasks,\n    >= 4001 and <= 4099 => Category.General,\n    >= 4100 and <= 4199 => Category.Evaluation,\n    _ => Category.Other\n};\n```\n\n### String Handling\n\n* Use `MSBuildNameIgnoreCaseComparer` for case-insensitive comparisons of MSBuild names; use `StringComparer.OrdinalIgnoreCase` only for non-MSBuild string comparisons\n* Use `char.ToUpperInvariant()` for single-character comparisons\n* Use `ReadOnlySpan<char>` and `Slice()` to avoid string allocations\n* Use `int.TryParse(span, out var result)` on .NET Core+ for allocation-free parsing\n\n### Inlining Hot Paths\n\n```csharp\n[MethodImpl(MethodImplOptions.AggressiveInlining)]\nprivate static bool IsCompilerPrefix(string value) => ...\n```\n\n### Immutable Collections\n\n**Build once, read many times** (most common in MSBuild):\n```csharp\nImmutableArray<string> items = source.Select(x => x.Name).ToImmutableArray();\nFrozenDictionary<string, int> lookup = pairs.ToFrozenDictionary(x => x.Key, x => x.Value);\n```\n\n**Build incrementally over time**:\n```csharp\nImmutableList<string> items = ...;  // Use when adding items one by one\nImmutableDictionary<string, int> lookup = ...;\n```\n\n## Building\n\nMost development can use `dotnet build`. For the most complete possible build, use `MSBuild.exe` from Visual Studio 2026. See the [Windows build prerequisites](README.md#building-msbuild-with-visual-studio-2026-on-windows).\n\nNEVER pipe MSBuild output to a file. If you want a list of errors, add `-flp:\"v=q;LogFile=ErrorsAndWarnings.log\"` to the arguments.\n\nWhen considering a subset of build errors, always look at the BEGINNING of the set, not the end.\n\n## Individual projects\n\nBuild individual projects with `dotnet msbuild {path/to/project.csproj} -v:q`.\n\n### Whole-repo Build Commands\n\n| Platform | Command | Timeout |\n|----------|---------|---------|\n| Windows | `.\\build.cmd -v quiet` | 300+ seconds (~2-3 minutes) |\n| macOS/Linux | `./build.sh -v quiet` | 300+ seconds (~2-3 minutes) |\n\n### Bootstrap Environment Setup\n\nAfter building the whole repo, to use the just-built MSBuild to build things use the bootstrap environment (described in [`skills/use-bootstrap-msbuild](.github/skills/use-bootstrap-msbuild/SKILL.md)).\n\n### Build Troubleshooting\n\n* If build fails with \"Could not resolve SDK\", run the bootstrap environment script\n* Verify `dotnet --version` shows the preview/internal version\n* Use repository sample projects for testing, not external projects\n* Build artifacts go to `./artifacts/` directory\n\n### Running Tests\n\n**Windows:**\n```cmd\n# Full test suite (~9 minutes) - NEVER CANCEL\n.\\build.cmd -test\n\n# Individual test project (recommended):\ndotnet test src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj\n```\n\n**macOS/Linux:**\n```bash\n# Full test suite (~9 minutes) - NEVER CANCEL\n./build.sh --test\n\n# Individual test project (recommended):\ndotnet test src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj\n```\n\nYou can run a single unit test with `-- --filter-method \"*{methodname}*\"`, for example `dotnet test src/Framework.UnitTests/ -- --filter-method \"*ExerciseBuildEventContext*\"`.\n\n### Test Verification\n\n* **Individual Test Project**: ~10-60 seconds per project\n* **Full Test Suite**: ~9 minutes\n\n## Project Layout and Architecture\n\n### Directory Structure\n\n```\nsrc/\n├── Build/                    # Core MSBuild engine (Microsoft.Build)\n├── Build.UnitTests/          # Unit tests for core engine\n├── MSBuild/                  # MSBuild command-line tool\n├── Framework/                # MSBuild Framework (Microsoft.Build.Framework)\n├── Framework.UnitTests/      # Unit tests for framework\n├── Tasks/                    # Built-in MSBuild tasks (Microsoft.Build.Tasks)\n├── Tasks.UnitTests/          # Unit tests for tasks\n├── Utilities/                # MSBuild utilities (Microsoft.Build.Utilities)\n├── Utilities.UnitTests/      # Unit tests for utilities\n├── Shared/                   # Shared code across assemblies\n└── Samples/                  # Sample projects and extensions\n\nartifacts/\n├── bin/                      # Built binaries\n│   └── bootstrap/\n│       └── core/\n│           └── MSBuild.dll   # Built MSBuild executable\n├── sdk-build-env.sh          # Bootstrap script (Linux/macOS)\n├── msbuild-build-env.bat     # Bootstrap script (Windows)\n└── packages/                 # Built NuGet packages\n\ndocumentation/\n├── wiki/                     # Developer documentation\n├── specs/                    # Technical specifications\n└── *.md                      # Various documentation files\n```\n\n### Key Configuration Files\n\n* **`global.json`**: Pins .NET SDK version - never modify without explicit request\n* **`NuGet.config`**: Package source configuration - never modify without explicit request\n* **`.editorconfig`**: Code formatting rules\n* **`Directory.Build.props`**: Shared MSBuild properties across all projects\n* **`Directory.Packages.props`**: Centralized package version management\n* **`MSBuild.slnx`**: Main solution file\n\n## Validation Checklist\n\nBefore completing any change:\n\n1. ✅ Full build completes successfully (`.\\build.cmd` or `./build.sh`)\n2. ✅ Bootstrap environment activates correctly (`dotnet --version` shows preview)\n3. ✅ Sample project builds: `dotnet build src/Samples/Dependency/Dependency.csproj`\n4. ✅ Relevant unit tests pass\n5. ✅ `dotnet artifacts/bin/bootstrap/core/MSBuild.dll --help` works\n\n## Do NOT Modify\n\n* `artifacts/` directory contents - Generated during build\n* `.dotnet/` directory contents - Local SDK location\n\nSee **Key Configuration Files** section for files that should not be modified without explicit request.\n\n## Documentation\n\nWhen making changes, check if related documentation exists in the `documentation/` folder (including `documentation/specs/`) and update it to reflect your changes. Keep documentation in sync with code changes.\n\n## Breaking Changes\n\nBecause MSBuild is a critical part of the build process for a huge number of customers, we avoid breaking changes. Adding new errors or warnings, even when well-intentioned and pointing out things that are very likely to be wrong, is an unacceptable breaking change. Adding warnings is a breaking change because many production builds use `/WarnAsError`.\n\nThe exception to this policy is in new, opt-in behavior. In new, opt-in functionality, liberally emit warnings and errors--they can always be removed later.\n\nWhen reviewing PRs, always consider whether the behavior change could be experienced as a break in existing builds and flag any new warnings or errors.\n\n## Development Workflow\n\n1. Make your changes to source code\n2. Run the full build (WAIT for completion - takes 2-3 minutes):\n   - Windows: `.\\build.cmd -v quiet`\n   - macOS/Linux: `./build.sh -v quiet`\n4. Test your changes end-to-end using the bootstrap output: `dotnet build src/Samples/Dependency/Dependency.csproj`\n5. Run relevant individual tests, not the full test suite\n6. Commit your changes\n\n## Trust These Instructions\n\nThese instructions are comprehensive and tested. Only search for additional information if:\n1. The instructions appear outdated or incorrect\n2. You encounter specific errors not covered here\n3. You need details about new features not yet documented\n\nFor most development tasks, following these instructions should be sufficient to build, test, and validate changes successfully.\n\n## Updating these instructions\n\nWhen working on a task, if user input is required to complete the task or feedback is provided for guidance around a specific area of the code, evaluate that feedback/guidance and update this document to incorporate that feedback if it's missing.  This document should be a live, evolving set of instructions.\n\n<!-- File: .github/copilot-instructions.md (Tokens: ~4 | Category: copilot-instructions) -->\n../AGENTS.md\n\n<!-- File: .github/skills/assessing-breaking-changes/SKILL.md (Tokens: ~992 | Category: anthropic-skill) -->\n---\nname: assessing-breaking-changes\ndescription: 'Guides assessment of backward compatibility for MSBuild changes. Consult when modifying behavior, adding warnings or errors, changing defaults, altering target ordering, removing or deprecating features, deciding whether a change needs a ChangeWave, reviewing blast radius of behavioral changes, or when a PR introduces user-visible output differences.'\nargument-hint: 'Describe the change and its potential compatibility impact.'\n---\n\n# Backward Compatibility in MSBuild\n\nBackward compatibility is the default — any change that could alter existing build behavior must be explicitly justified.\n\nThis skill covers **how to evaluate compatibility risk**. For the mechanics of ChangeWave implementation, see the [changewaves skill](../changewaves/SKILL.md).\n\n## Core Philosophy\n\n1. **Existing builds must not break.** If a project built successfully yesterday, it must build successfully today with identical semantics.\n2. **New warnings are breaking changes.** Builds that use `-WarnAsError` or `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` will fail if you introduce a new warning. Always gate new warnings behind a ChangeWave or emit them as `Message` importance instead.\n3. **Output changes are behavioral changes.** Even \"improvements\" to output formatting, file paths, or diagnostic text can break downstream consumers that parse MSBuild output.\n4. **Removal is nearly impossible.** Never remove CLI switches, public APIs, or property names. Deprecate with warnings first, then gate removal behind a ChangeWave after multiple release cycles.\n\n## Blast Radius Checklist\n\nBefore merging any behavioral change, evaluate:\n\n| Question | If Yes |\n|----------|--------|\n| Does this change what gets built or how? | ChangeWave required |\n| Does this add a new warning? | ChangeWave required (WarnAsError impact) |\n| Does this change a property default? | ChangeWave required (existing .csproj files depend on defaults) |\n| Does this alter target execution order? | Test with real-world solutions; likely ChangeWave |\n| Does this change console or binlog output format? | Consider downstream tool impact |\n| Does this affect only internal code paths with no user-visible effect? | No ChangeWave needed |\n| Is this a pure bug fix restoring documented behavior? | Usually no ChangeWave; use judgment on blast radius |\n\n## When ChangeWave Is NOT Needed\n\n- Internal refactoring with no observable behavior change\n- Performance improvements that don't change semantics\n- New opt-in features gated by a new property or switch\n- Bug fixes that restore clearly-intended behavior with limited blast radius\n\nFor detailed ChangeWave mechanics, see [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md) and [ChangeWaves.md](../../../documentation/wiki/ChangeWaves.md).\n\n## The Warnings-as-Errors Rule\n\nThis is the most commonly missed compatibility concern:\n\n```xml\n<!-- Many enterprise builds set this globally -->\n<TreatWarningsAsErrors>true</TreatWarningsAsErrors>\n```\n\nAny new `MSBxxxx` warning you add will **break these builds**. Options:\n1. **Gate behind ChangeWave** — preferred for genuinely important warnings\n2. **Use `MessageImportance.Low` or `Normal`** — for informational diagnostics\n3. **Add as an error from the start** — if the condition is always wrong\n\n## Deprecation Protocol\n\n1. Add a deprecation warning (behind ChangeWave if broad impact)\n2. Document the deprecation in release notes\n3. Maintain the old behavior for at least two major .NET versions\n4. Only remove after the ChangeWave has rotated out\n\n## Compatibility Test Matrix\n\nWhen testing backward compatibility, verify:\n\n- **Multi-targeting projects** — `<TargetFrameworks>net472;net8.0</TargetFrameworks>`\n- **Solution builds with mixed project types** — C#, F#, VB, C++/CLI\n- **Incremental builds** — second build should be a no-op\n- **Design-time builds** — Visual Studio calls different target contracts\n- **Cross-platform** — path separator differences, case sensitivity on Linux\n- **WarnAsError builds** — explicitly test with `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>`\n\n## Decision Framework\n\n```\nIs the change user-visible?\n├── No → Ship it (no ChangeWave needed)\n└── Yes\n    ├── Is it a new opt-in feature? → Ship it (no ChangeWave needed)\n    └── Does it change existing behavior?\n        ├── Bug fix with low blast radius? → Ship it, add regression test\n        └── Behavioral change or new warning?\n            └── Gate behind ChangeWave, test opt-out path\n```\n\nDocument the compatibility decision in your PR description so reviewers can validate it.\n\n<!-- File: .github/skills/authoring-errors-and-warnings/SKILL.md (Tokens: ~1300 | Category: anthropic-skill) -->\n---\nname: authoring-errors-and-warnings\ndescription: 'Guides authoring of MSBuild errors, warnings, and diagnostic messages. Consult when adding new MSBxxxx codes, writing or modifying user-facing diagnostic text, deciding between error/warning/message severity, working with Strings.resx resource files, formatting paths in error output, or evaluating whether a new warning could break WarnAsError builds.'\nargument-hint: 'Describe the error/warning being added or modified.'\n---\n\n# Error and Warning Authoring in MSBuild\n\nError messages are MSBuild's primary user interface. They must help developers fix problems without reading source code.\n\nFor the mechanics of error code assignment, see [assigning-msb-error-code.md](../../../documentation/assigning-msb-error-code.md).\n\n## Error Message Quality Rules\n\nEvery error message must answer three questions:\n\n1. **What happened?** — State the problem clearly\n2. **Why?** — Provide context (file, property, expected value)\n3. **What should the user do?** — Give actionable guidance\n\n```xml\n<!-- BAD: What is \"it\"? What should I do? -->\n<value>MSB4999: Invalid configuration.</value>\n\n<!-- GOOD: States the problem, context, and fix -->\n<value>MSB4999: The project \"{0}\" specifies TargetFramework \"{1}\" which is not installed. Install the SDK for \"{1}\" or update the TargetFramework in the project file.</value>\n```\n\n## Severity Decision Framework\n\n```\nIs the condition always wrong (invalid input, impossible state)?\n├── Yes → ERROR (build should fail)\n└── No\n    ├── Could this cause subtle build correctness issues?\n    │   ├── Yes, likely → WARNING (but see WarnAsError impact below)\n    │   └── Yes, maybe → MESSAGE at Normal importance\n    └── Is this purely informational?\n        └── Yes → MESSAGE at Low importance\n```\n\n### The WarnAsError Constraint\n\n**New warnings are breaking changes** for builds using:\n- `-WarnAsError` (CLI)\n- `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>` (project)\n- `<WarningsAsErrors>MSBxxxx</WarningsAsErrors>` (specific codes)\n\nBefore adding a new warning, consider:\n1. **Is the warning important enough to justify breaking WarnAsError builds?** If yes, gate behind a ChangeWave (see [changewaves skill](../changewaves/SKILL.md)).\n2. **Could this be a Message instead?** Messages never break builds but still appear in binary logs.\n3. **Should this be an error from the start?** If the condition is always wrong, skip the warning stage.\n\n## MSB Error Code Assignment\n\n### Code Ranges\n\n| Range | Area |\n|-------|------|\n| `MSB1xxx` | Command-line handling |\n| `MSB3xxx` | Tasks (`Microsoft.Build.Tasks.Core.dll`) |\n| `MSB4xxx` | Engine (`Microsoft.Build.dll`) |\n| `MSB5xxx` | Shared code across assemblies |\n| `MSB6xxx` | Utilities (`Microsoft.Build.Utilities`) |\n\n### Assignment Process\n\n1. Open the `Strings.resx` file for the appropriate assembly\n2. Find the \"Next message code\" comment at the bottom of the file\n3. Search the repo to confirm the code is not already in use\n4. Use the code; update the comment to the next available number\n\nSee [assigning-msb-error-code.md](../../../documentation/assigning-msb-error-code.md) for detailed steps.\n\n## Resource String Format\n\n```xml\n<data name=\"FeatureArea.DescriptiveName\">\n  <value>MSBxxxx: Clear description with {0} placeholders for runtime values.</value>\n  <comment>{StrBegin=\"MSBxxxx: \"}{0} is the project file path. {1} is the property name.</comment>\n</data>\n```\n\n### Rules\n\n- Resource name uses `FeatureArea.DescriptiveName` convention\n- Value starts with `MSBxxxx: ` (code, colon, space)\n- Comment documents the `{StrBegin}` marker and explains each `{N}` placeholder\n- Comments guide translators — mention untranslatable tokens\n\n## Consuming Error Resources in Code\n\nUse the standard formatting method that extracts and applies the error code. Engine-level project\nerrors use `Microsoft.Build.Internal.ProjectErrorUtilities`:\n\n```csharp\n// For errors\nLog.LogErrorWithCodeFromResources(\"Copy.Error\", sourceFile, destFile, ex.Message);\n\n// For warnings\nLog.LogWarningWithCodeFromResources(\"ResolveAssemblyReference.Conflict\", assemblyName);\n\n// For engine-level errors (not in tasks)\nProjectErrorUtilities.ThrowInvalidProject(\n    elementLocation,\n    \"InvalidProjectFile\",\n    arg1, arg2);\n```\n\n**Never** construct error messages by string concatenation — always use resource strings for localization support.\n\n## Localization Requirements\n\n- Error **codes** (`MSBxxxx`) are never localized\n- Error **text** is localized into all supported languages\n- After adding/modifying resource strings, run a full build to generate `.xlf` placeholder translations\n- Use `<comment>` elements to help translators understand context\n- See [Localization.md](../../../documentation/wiki/Localization.md) for the full localization process\n\n## Path Formatting in Error Messages\n\n- Show paths relative to the project directory when possible\n- Use the path exactly as the user specified it (don't normalize slashes)\n- Include file and line number via `IElementLocation` when available — this enables IDE click-to-navigate\n- For paths that could contain spaces, ensure they're quoted in the message\n\n## Checklist for New Errors/Warnings\n\n- [ ] Error code assigned from correct `Strings.resx` range\n- [ ] \"Next message code\" comment updated in the resx\n- [ ] Message answers: what happened, why, what to do\n- [ ] `{StrBegin}` comment and placeholder documentation added\n- [ ] Severity chosen (error vs warning vs message) with WarnAsError considered\n- [ ] If warning: ChangeWave gating evaluated\n- [ ] Full build run to generate `.xlf` files\n- [ ] Test verifies the error is produced with correct code and text\n\n<!-- File: .github/skills/changewaves/SKILL.md (Tokens: ~901 | Category: anthropic-skill) -->\n---\nname: changewaves\ndescription: 'Manage MSBuild Change Waves: create new waves, condition features behind opt-out flags, write tests for wave-gated features, document change waves in ChangeWaves.md, and retire expired waves. Use when adding changes that need an opt-out or rotating out old change waves. Changes that introduce a user-visible behavior change should consider whether to use a changewave.'\nargument-hint: 'Add, query, or remove changewaves and changewave checks.'\n---\n\n# Managing MSBuild Change Waves\n\nA Change Wave is an opt-out flag that groups risky features together. Users disable features by setting the environment variable `MSBUILDDISABLEFEATURESFROMVERSION` to the wave version. This skill covers the **how** — the full lifecycle: creating a wave, conditioning code on it, testing, documenting, and retiring.\n\nFor the **when** — deciding whether a change is a breaking change and whether it needs a ChangeWave at all — see [assessing-breaking-changes](../assessing-breaking-changes/SKILL.md).\n\n\n## Decide Whether a Change Wave Is Appropriate\n\nUse a change wave when a change is valuable but has meaningful compatibility risk for existing builds.\n\nGood candidates:\n- User-visible behavior changes that may regress some build graphs.\n- Changes in parsing/evaluation/execution semantics where real-world usage is broad or hard to predict.\n- Changes that may require customer adaptation time and benefit from a temporary opt-out.\n\nUsually avoid a change wave for:\n- Pure bug fixes that restore intended existing behavior with low compatibility risk.\n- Internal refactoring/perf work with no externally observable behavior change.\n- New functionality that is already explicitly opt-in (for example, gated by a new property, switch, or API surface).\n\nIf uncertain, default to safety:\n1. Assess blast radius and rollback difficulty.\n2. Check whether the change could be experienced as a breaking change in production builds.\n3. Use a wave if an opt-out is likely needed while customers adapt.\n\nDocument the decision in PR notes so reviewers can validate the call.\n\nIf a changewave is appropriate, consult [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md) (developer-facing) and [ChangeWaves.md](../../../documentation/wiki/ChangeWaves.md) (public-facing) for details on how to follow the below steps.\n\n## Step 1: Determine the Correct Wave Version\n\nLook up the current MSBuild version. If a wave already exists for that version in `src/Framework/ChangeWaves.cs`, use it. Otherwise, create a new one.\n\n## Step 2: Condition Your Feature on the Wave\n\nC# and MSBuild examples are in [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md). Prefer to put checks inline rather than abstracting out a method to host the check.\n\n## Step 3: Write Tests\n\nWrite normal tests for the new behavior. Then add at least one test that verifies the old behavior is **preserved** when the wave is opted out.\n\n## Step 4: Document the Feature\n\nAdd an entry to the **Current Rotation of Change Waves** section in [ChangeWaves.md](../../../documentation/wiki/ChangeWaves.md).\n\n- If a heading for this wave already exists, add a bullet under it.\n- If not, add a new `### {Major}.{Minor}` heading at the top of the current rotation list.\n\nEach entry is a bullet with a link to the PR:\n\n```markdown\n### 18.6\n- [Short description of the change.](https://github.com/dotnet/msbuild/pull/NNNNN)\n```\n\n## Only when explicitly requested: Retire an Expired Wave\n\nWaves rotate out when a new major .NET version will be released. When retiring a wave, follow the detailed description in [ChangeWaves-Dev.md](../../../documentation/wiki/ChangeWaves-Dev.md).\n\n## Checklist\n\n- [ ] Decision made: change wave needed?\n- [ ] Correct wave version chosen from `eng/Versions.props`\n- [ ] Wave field and `AllWaves` entry added in `ChangeWaves.cs` (if new wave)\n- [ ] Feature code wrapped with `AreFeaturesEnabled`\n- [ ] Test verifying opt-out disables the feature\n- [ ] Entry added to `documentation/wiki/ChangeWaves.md`\n\n<!-- File: .github/skills/cswin32-com/SKILL.md (Tokens: ~4380 | Category: anthropic-skill) -->\n---\nname: cswin32-com\ndescription: 'Guides struct-based COM interop in MSBuild using CsWin32 patterns. Consult when working with the generic ComScope type, ComClassFactory, IComIID, the IID.Get generic method, delegate* unmanaged vtables, CoCreateInstance, or manually defining COM interfaces not in Win32 metadata (e.g. WMI IWbemLocator, IWbemServices).'\nargument-hint: 'Describe the COM interface or activation pattern you are working with.'\n---\n\n# CsWin32 COM Interop Guide\n\nStruct-based COM interop using CsWin32 patterns — AOT-compatible, no `[ComImport]` or built-in marshalling.\n\n**Paired skill:** [cswin32-interop](../cswin32-interop/SKILL.md) covers general P/Invoke and the blittable signature rules that apply to both `[DllImport]` and COM vtables. This file covers only the COM-specific layer.\n\n## Workflow\n\n1. **Interface in Win32 metadata?** Add the name to `src/Framework/NativeMethods.txt` → CsWin32 generates it. Not in metadata (WMI, Fusion, Setup Configuration) → define a manual struct under its own folder, excluded from source builds via `<Compile Remove>`.\n2. **Lifetime: `using ComScope<T> scope = new();`** for every transient COM pointer (`CoCreateInstance`, `QueryInterface`, `IEnumXxx::Next`, factory output, app-local `STDAPI Get*`, etc.). Never write `T* local; try { ... } finally { local->Release(); }` — that's the pre-`ComScope` shape that leaks on every early return. Same for `BSTR` out-params: `using BSTR x = default;`.\n3. **Activate** via `ComClassFactory.TryCreate(CLSID, ...)` (AOT-compatible) or `PInvoke.CoCreateInstance` with `IID.Get<T>()` — not `&localGuid`.\n4. **Call** via `scope.Pointer->Method(...)`. Pass `ComScope<T>` directly where the API expects `T**` / `void**` — the implicit operator does the address-of.\n5. **Match the caller's error contract.** If the top-level consumer swallows COM failure (\"no result\" == \"absent\"), helpers return `default` / `false` instead of throwing a `COMException` that's immediately discarded. `ThrowOnFailure` only when the exception will actually propagate or assert a should-never-happen.\n6. **Guard with `#if FEATURE_WINDOWSINTEROP`** — add `&& NET` only when the struct uses the static-abstract `IComIID` form exclusively (no net472 dual-target).\n\n## Manual COM Structs (Not in Metadata)\n\nDefine each interface in its own file under e.g. `src/Tasks/AssemblyDependency/Fusion/`, `src/Framework/Shared/VisualStudio/`, `src/Framework/Utilities/Wmi/`. Exclude from source builds. Pattern (dual-target — works on net472 + .NET):\n\n```csharp\ninternal unsafe struct IAssemblyCache : IComIID\n{\n    public static readonly Guid IID_IAssemblyCache = new(0xE707DCDE, ...);\n\n#if NET\n    static ref readonly Guid IComIID.Guid\n    {\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        get => ref Unsafe.AsRef(in IID_IAssemblyCache);\n    }\n#else\n    readonly ref readonly Guid IComIID.Guid\n    {\n        [MethodImpl(MethodImplOptions.AggressiveInlining)]\n        get => ref Unsafe.AsRef(in IID_IAssemblyCache);\n    }\n#endif\n\n    private readonly void** _lpVtbl;\n\n    // IUnknown at indices 0-2, then interface methods at 3+\n    public HRESULT QueryInterface(Guid* riid, void** ppvObject)\n    {\n        fixed (IAssemblyCache* pThis = &this)\n            return ((delegate* unmanaged[Stdcall]<IAssemblyCache*, Guid*, void**, HRESULT>)_lpVtbl[0])(pThis, riid, ppvObject);\n    }\n    // AddRef @1, Release @2, then interface methods at correct slot indices.\n}\n```\n\n**Rules:**\n- `delegate* unmanaged[Stdcall]` for the function-pointer cast — IDL `STDMETHODCALLTYPE` ⇒ `__stdcall` on Win32. Picking the wrong convention silently corrupts the stack.\n- `IComIID` has two shapes: static-abstract (`static ref readonly Guid IComIID.Guid`) on .NET 7+, instance (`readonly ref readonly Guid IComIID.Guid`) on net472 / netstandard2.0. CsWin32 picks the right one for its generated structs automatically (see [IComIID across TFMs](#icomiid-across-tfms)); a **manual** struct must spell out both arms itself. For .NET-only structs (WMI), drop the `#else` branch and gate the whole file `#if NET`. For dual-target structs (Fusion), keep both as shown — see `src/Tasks/AssemblyDependency/Fusion/` for the canonical example.\n- Use CsWin32-generated `PCWSTR` / `PWSTR` for wide strings (add to `NativeMethods.txt`); raw `char*` only when no typed equivalent exists.\n- Vtable slots are exact: index 0 = `QueryInterface`, 1 = `AddRef`, 2 = `Release`, 3+ = interface methods in IDL order. When inheriting, **add the parent interface's method count** before counting your own (e.g. `ISetupConfiguration2.EnumAllInstances` is at slot 6 = 3 IUnknown + 3 v1 + 0).\n- CS0592 prevents `[SupportedOSPlatform]` on structs — put it on individual methods.\n\n## Lifetime & Access\n\n**A raw `T*` (COM struct) is forbidden as a field on a non-`ref` type.** Allowed locations: locals in `unsafe` methods, parameters, fields of a `ref struct`. Anywhere else (instance fields of `class` / non-`ref` `struct`) → use `AgileComPointer<T>` — a raw pointer field is an apartment-agility hazard and leaks on finalize-without-Dispose.\n\n### `ComScope<T>` — transient pointers\n\n`ref struct`, `using`'d, calls `Release` on dispose. **The default for everything that doesn't survive the current method.** Implicitly converts to `T**` and `void**`, so out-params write into the scope directly:\n\n```csharp\nusing ComScope<ISomeInterface> scope = new();\nGuid clsid = SomeStruct.CLSID;\nGuid iid = IID.Get<ISomeInterface>();\nPInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &iid, scope).ThrowOnFailure();\nscope.Pointer->DoThing(...);\n\nusing ComScope<IOther> other = new();\nGuid otherIid = IOther.IID_IOther;\nscope.Pointer->QueryInterface(&otherIid, other).ThrowOnFailure();\n```\n\n- Access methods via `scope.Pointer->Method(...)`. Null-check with `scope.IsNull`.\n- Applies to *every* COM out-param: `CoCreateInstance`, `QueryInterface`, `IEnumXxx::Next`, factory methods, app-local `STDAPI Get*` (declare the `[DllImport]` with `T** pp`, not `out T*`, so the implicit operator binds).\n\n**Ownership transfer out of a helper.** A helper that acquires a COM pointer can return `ComScope<T>` directly; intermediate pointers stay in their own `using` scopes and Release on the helper's `return`. `default` `ComScope<T>` is null and its `Dispose` is a no-op, so callers don't need an extra null guard:\n\n```csharp\nprivate static ComScope<ISetupConfiguration2> AcquireSetupConfig()\n{\n    Guid clsid = SetupConfiguration.CLSID_SetupConfiguration;\n    Guid iid = ISetupConfiguration2.IID_ISetupConfiguration2;\n\n    ComScope<ISetupConfiguration2> config = new();\n    HRESULT hr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, &iid, config);\n    if (hr.Succeeded) return config;                 // ownership flows to caller\n    if (hr != HRESULT.REGDB_E_CLASSNOTREG) return default;\n\n    using ComScope<ISetupConfiguration> v1 = new();  // intermediate, Released on return\n    if (SetupConfiguration.GetSetupConfiguration(v1, IntPtr.Zero) < 0 || v1.IsNull) return default;\n    return v1.Pointer->QueryInterface(&iid, config).Succeeded ? config : default;\n}\n\n// caller:\nusing ComScope<ISetupConfiguration2> config = AcquireSetupConfig();\nif (!config.IsNull) { /* use config.Pointer */ }\n```\n\n### `BSTR` — COM strings\n\nCsWin32-generated; extended in [`Windows/Win32/Foundation/BSTR.cs`](../../../src/Framework/Windows/Win32/Foundation/BSTR.cs) with `IDisposable`. `Dispose` calls `SysFreeString` and zeroes the field in place. **Scope every COM `BSTR` out-param with `using BSTR x = default;`** — no manual `try/finally + SysFreeString`:\n\n```csharp\nusing BSTR versionBstr = default;\nif (instance->GetInstallationVersion(&versionBstr).Failed) return false;\nstring version = versionBstr.ToString();\n// SysFreeString runs at scope exit.\n```\n\nIn-place-only: `BSTR.Dispose` writes through `Unsafe.AsRef(in this)`, so `using` must be on the storage location, not on a method-returned copy. Same rule as `ComScope<T>`.\n\n### `AgileComPointer<T>` — fields outliving a method\n\nFinalizable managed class. Use whenever a COM pointer is stored in a class field (the only legal way — raw `T*` fields are forbidden, see top of section).\n\n- Registers in the Global Interface Table (thread-agile); finalizer releases if `Dispose` is missed.\n- Access via `using ComScope<T> scope = agile.GetInterface();` then `scope.Pointer->Method(...)`. `GetInterface()` round-trips through the GIT — hoist a single scope to the top of a method when several calls share it.\n- Constructor `takeOwnership`: pass `false` when the raw pointer is also held by a `ComScope<T>` that will Release (GIT AddRefs independently); pass `true` only when no other code path will Release.\n- Dispose via the owner's `Dispose` / `DisposeManagedResources` (`AgileComPointer` is managed, not unmanaged — its finalizer is the safety net, not the primary cleanup).\n\n## Activation\n\n```csharp\n// AOT-compatible\nif (ComClassFactory.TryCreate(IWbemLocator.CLSID, out var factory, out HRESULT hr))\n    using ComScope<IWbemLocator> instance = factory.TryCreateInstance<IWbemLocator>(out hr);\n\n// CoCreateInstance — IID.Get<T>(), not &localGuid\nGuid clsid = IWbemLocator.CLSID;\nusing ComScope<IWbemLocator> locator = new();\nhr = PInvoke.CoCreateInstance(&clsid, null, CLSCTX.CLSCTX_INPROC_SERVER, IID.Get<IWbemLocator>(), locator);\n```\n\n## Error-Handling Parity When Migrating\n\nStruct-based COM returns raw `HRESULT`; `[ComImport]` and built-in activation threw automatically. Preserve the old throw-vs-return at each call site:\n\n| Old shape | Threw via | Migrated shape |\n|---|---|---|\n| `new SomeCoClass()` | built-in activation | `PInvoke.CoCreateInstance(...).ThrowOnFailure()` |\n| `(IFoo)rcw` cast | `InvalidCastException` on QI | `QueryInterface(&iid, scope).ThrowOnFailure()` |\n| `[ComImport]` method, default `PreserveSig=false` | marshaller throws on `FAILED(hr)` | `.ThrowOnFailure()` at the call site |\n| `[ComImport]` method with `[PreserveSig]` | caller inspects return | mirror the existing `hr` branch — don't start throwing |\n\n**Factory-method exception:** when the old contract was \"return null for invalid input\" (e.g. `Create(path)`), keep null-return only for the legitimate-rejection path; activation / QI failures still throw. See [`MetadataReader.cs`](../../../src/Tasks/ManifestUtil/MetadataReader.cs) (throws on activation/QI, returns null on `OpenScope`).\n\n**Don't throw when the caller swallows it.** If the top-level consumer wraps the whole chain in `catch (COMException) { }`, inner helpers must return `default` / `false` / empty instead of constructing a `COMException` only to have it discarded. Throwing here allocates the exception, walks the stack, and hides the HRESULT for no benefit. See [`VisualStudioLocationHelper.AcquireSetupConfiguration2`](../../../src/Framework/VisualStudioLocationHelper.cs).\n\n## Mocking Struct-Based COM in Tests\n\nStruct-based COM calls go through raw vtable pointers, so a managed mock can't be passed where a `T*` is expected. Bridge with the **built-in COM marshaller**: the mock implements the *managed* interface (BCL `System.Runtime.InteropServices.ComTypes.*` when ABI-compatible — true for `ITypeLib` / `ITypeInfo` — else CsWin32's nested `[ComImport] internal interface Interface`), and the test passes a **CCW** (COM-callable wrapper) pointer to the code under test:\n\n```csharp\n// MockTypeLib : ComTypes.ITypeLib   (a normal managed class)\nIntPtr ccw = Marshal.GetComInterfaceForObject(mock, typeof(ComTypes.ITypeLib));\ntry { walker.AnalyzeTypeLibrary((ITypeLib*)ccw); }   // struct API; real QueryInterface works\nfinally { Marshal.Release(ccw); }\n```\n\nThe CCW exposes a real vtable, so no hand-rolled native vtable is needed and the managed mock framework stays intact. But the mock must now behave like real COM, not a managed object:\n\n- **All `[out]` params are written**, even ones the caller ignores. Passing `null` for a trailing out-param (`GetDocumentation(-1, &name, null, null, null)`) faults the marshaller — supply a real pointer for each.\n- **Exception identity is lost.** An HRESULT-returning method resurfaces a thrown exception as a *new* `COMException` (HRESULT preserved, instance not); a `uint`/`void` method with no HRESULT channel (`GetTypeInfoCount`, the `Release*` methods) **swallows it silently** and returns garbage. Assert on type/count, not the injected instance, and don't rely on `void`/`uint` methods to propagate.\n- **Throw `COMException` on bad input, don't `Assert`.** An `Assert` throws an `XunitException` that escapes the SUT's `catch (COMException)`; a `COMException` becomes a failing HRESULT the SUT handles normally. (Matters because a swallowed `GetTypeInfoCount` failure can feed an out-of-range index to the next call.)\n\nSee [`MockTypeLib.cs`](../../../src/Tasks.UnitTests/MockTypeLib.cs) / [`MockTypeInfo.cs`](../../../src/Tasks.UnitTests/MockTypeInfo.cs) and the CCW bridge in [`ComReferenceWalker_Tests.cs`](../../../src/Tasks.UnitTests/ComReferenceWalker_Tests.cs).\n\n## Strongly-typed handle / token wrappers\n\nWhen the native side `typedef`s a primitive into a family of \"same shape, different meaning\" aliases (`corhdr.h`'s `typedef ULONG32 mdToken; typedef mdToken mdAssembly; ...`), mirror that hierarchy with distinct `readonly struct` wrappers holding the single underlying primitive — same pattern as CsWin32's `HANDLE` / `HWND` / `HMODULE`. Stays blittable; `delegate*` casts and arrays marshal at zero cost.\n\nConversions follow the typedef hierarchy: **implicit** widening to the base (`MdAssembly` → `MdToken`, always safe), **explicit** narrowing (`(MdAssembly)token`, opt-in because the C side can't enforce the kind at the cast site).\n\n**Check the native header for the canonical validation primitives** before defining `IsNil` / `IsValid`. Encoding details often hide in macros that don't grep cleanly. For `mdToken` the encoding is `(TableType << 24) | Rid`:\n\n| C macro | Meaning |\n|---|---|\n| `TypeFromToken(tk) = tk & 0xff000000` | Table-type tag (high byte) → `CorTokenType` |\n| `RidFromToken(tk) = tk & 0x00ffffff`  | Row id (low 24 bits) |\n| `IsNilToken(tk) = RidFromToken(tk) == 0` | Nil check — **rid half, not the whole value** |\n| `mdAssemblyNil = mdtAssembly = 0x20000000` | Per-type nil is the table-type tag, **not 0** |\n\nA naive `IsNil => Value == 0` would misclassify the `0x20000000` \"no assembly in this scope\" return from `GetAssemblyFromScope`. Mirror the macro: `IsNil => Rid == 0`. Reference: [`Tokens.cs`](../../../src/Tasks/AssemblyDependency/Metadata/Tokens.cs), [`CorTokenType.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorTokenType.cs).\n\n## IComIID across TFMs\n\nCsWin32 (since **0.3.287**) emits `IComIID` and attaches it to every generated COM struct on **all** target frameworks — static-abstract `static ref readonly Guid Guid { get; }` on .NET 7+, instance `ref readonly Guid Guid { get; }` on net472 / netstandard2.0. No hand-authored polyfill is needed; the old `src/Framework/Polyfills/IComIID.cs` + `IComIIDPolyfills.cs` files (one `partial` per generated struct) are gone.\n\nWhen you add a new CsWin32-generated COM type to `NativeMethods.txt`, it carries `IComIID` automatically through `ComScope<T>` — nothing extra to do.\n\nOnly **manual** structs (WMI, Fusion, Setup Configuration, CLR metadata) implement `IComIID` by hand, matching the per-TFM shape — see [Manual COM Structs](#manual-com-structs-not-in-metadata). `IID.Get<T>()` ([`IID.cs`](../../../src/Framework/Windows/Win32/IID.cs)) reads it via `T.Guid` on .NET and `default(T).Guid` on net472.\n\n## File Organization\n\n| Location | Contents |\n|---|---|\n| `src/Framework/Windows/Win32/System/Com/` | `ComScope`, `ComClassFactory`, `AgileComPointer`, `GlobalInterfaceTable` |\n| `src/Framework/Windows/Win32/IID.cs` | Generic IID lookup |\n| `src/Framework/Utilities/Wmi/`, `src/Framework/Shared/VisualStudio/`, `src/Tasks/AssemblyDependency/Fusion/`, `src/Tasks/AssemblyDependency/Metadata/`, `src/Tasks/TypeLib/` | Manual COM struct interfaces — own folder per API surface |\n\n## CS3016 CLS Compliance\n\nCsWin32 (since **0.3.287**) auto-emits `[CLSCompliant(false)]` on every generated COM struct wrapper that carries CCW thunks (the `[UnmanagedCallersOnly(CallConvs = new[] { ... })]` array argument is what trips CS3016), and adds CS3019 / CS3021 to its own generated-file `#pragma warning disable` list. So a CLS-compliant assembly (`[assembly: CLSCompliant(true)]`) builds clean with no consumer action — the hand-authored `GeneratedInteropClsCompliance.cs` partial and the `.editorconfig` CS3019 suppression for `{**/Windows/**/*.cs}` are gone.\n\nThe attribute is only emitted on .NET 5+ TFMs (net472 / netstandard2.0 have no CCW thunks, so no CS3016) and only for `internal` wrappers (the generator default). Don't re-add per-type `[CLSCompliant(false)]` partials or blanket `NoWarn`. See https://github.com/dotnet/roslyn/issues/68526 for the underlying Roslyn limitation that motivated the generator-side fix.\n\n<!-- File: .github/skills/cswin32-interop/SKILL.md (Tokens: ~3532 | Category: anthropic-skill) -->\n---\nname: cswin32-interop\ndescription: 'Guides CsWin32 P/Invoke interop in MSBuild. Consult when working with the PInvoke class, Windows.Win32 namespaces, FEATURE_WINDOWSINTEROP, HANDLE/HMODULE/HRESULT types, the generic BufferScope type, replacing [DllImport] with CsWin32, or conditioning Windows-only code for source builds.'\nargument-hint: 'Describe the Windows API or interop code you are migrating or adding.'\n---\n\n# CsWin32 Interop Guide\n\n[CsWin32](https://github.com/microsoft/CsWin32) replaces `[DllImport]` with source-generated `PInvoke.*` calls. `FEATURE_WINDOWSINTEROP` is the compile-time gate; source builds disable it.\n\n**Paired skill:** [cswin32-com](../cswin32-com/SKILL.md) covers struct-based COM interop on top of CsWin32 (`ComScope<T>`, `AgileComPointer<T>`, `delegate* unmanaged` vtables, `IComIID`, `CoCreateInstance`, manual COM structs not in Win32 metadata). This file covers only the general P/Invoke layer; the COM skill builds on its blittable-signature rules.\n\n## Rules\n\n1. **Replace `[DllImport]` with `PInvoke.*`**. Delete old declarations and hand-written structs/enums/constants.\n2. **Gate with `#if FEATURE_WINDOWSINTEROP`**, add runtime `IsWindows` check inside. Both required.\n3. **Use CsWin32 types directly** (`HANDLE`, `HMODULE`, `HRESULT.S_OK`, `FILE_FLAGS_AND_ATTRIBUTES`, etc.).\n4. **Call `PInvoke.*` directly** — no wrappers. Types flow via `InternalsVisibleTo`.\n5. **Prefer CsWin32 for Windows APIs**. Use `[LibraryImport]` only for non-Windows native calls (e.g. `libc`), guarded with `#if NET`.\n6. **Preserve the old error-handling contract.** Check the original `[DllImport]` for `PreserveSig` / `SetLastError` / `BOOL` / `HRESULT` semantics and reproduce them: `PreserveSig=false` → `.ThrowOnFailure()`; `SetLastError=true` + failed `BOOL` → `throw new Win32Exception()`. Silently returning where the old code threw is a behavior change. See [cswin32-com's parity table](../cswin32-com/SKILL.md#error-handling-parity-when-migrating) for the COM-side equivalent.\n\n## Blittable signatures\n\nCsWin32 is configured with `allowMarshaling: false`, so every `[DllImport]` and every manual\nCOM vtable method must be blittable — no marshalling at the boundary. These rules apply\nto both. For COM-vtable-only additions, see\n[cswin32-com](../cswin32-com/SKILL.md#blittable-vtable-signatures).\n\n- **Return `HRESULT`** from HRESULT-returning APIs (not `int`). Blittable (single `int` field),\n  exposes `.Succeeded` / `.Failed` / `.ThrowOnFailure()`. Use `HRESULT.S_OK` over `0`; cast\n  `e.HResult` to `(HRESULT)` when wrapping. `AddRef` / `Release` return `uint`.\n- **Call `.ThrowOnFailure()`** instead of `if (hr.Failed) Marshal.ThrowExceptionForHR(hr)` —\n  same exception, IErrorInfo-enriched, one-line call site:\n  `iface->Method(...).ThrowOnFailure();`. Branch on `hr` only when handling a specific HRESULT\n  (e.g. `ERROR_INSUFFICIENT_BUFFER`) before throwing. See\n  [cswin32-com's parity table](../cswin32-com/SKILL.md#error-handling-parity-when-migrating)\n  for the migration contract.\n- **Use `PCWSTR` / `PWSTR`** for wide strings, never managed `string`. Implicit conversion from\n  `fixed (char* p = managedString)`. Add to `NativeMethods.txt` if not yet generated.\n- **`T**` not `out T*`** for pointer outputs. `out` triggers marshaling + a `fixed` round-trip\n  at every call site.\n- **`void*` for opaque / reserved params** — never `IntPtr.Zero`; pass `null` literally.\n  `IntPtr` is fine at boundaries with the wider .NET surface (`Marshal.*`,\n  `SafeHandle.DangerousGetHandle`, public API).\n- **Prefer `nint` / `nuint`** over `IntPtr` / `UIntPtr` for native-sized integers — no boxing,\n  better cast semantics, no `IntPtr.Zero` ceremony.\n- **No managed reference types** (`string`, `StringBuilder`, arrays) in blittable signatures.\n- **Don't specify `PreserveSig = true` on `[DllImport]`** — it's the default. Use\n  `PreserveSig = false` only to force marshaller throw-on-failure (rare; prefer returning\n  `HRESULT` and `.ThrowOnFailure()`). `[ComImport]` defaults the opposite way, but struct-based\n  COM uses raw `delegate*` and isn't affected — see\n  [cswin32-com](../cswin32-com/SKILL.md#blittable-vtable-signatures).\n- **Constrain flag / option parameters to a typed `enum`.** When a native `DWORD` / `ULONG` /\n  `int` is documented as a `typedef enum` or `#define` set, declare a C# `[Flags] enum Foo : uint`\n  (matching the underlying type) and use it in the signature (and `delegate*` cast for COM).\n  Mirror the constraint even when the native side has no named enum. Self-documenting at the\n  call site: `OpenScope(path, CorOpenFlags.ofRead, ...)` vs `OpenScope(path, 0, ...)`. Co-locate\n  the enum next to its consumer. See\n  [`CorOpenFlags.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorOpenFlags.cs) and\n  [`CorAssemblyFlags.cs`](../../../src/Tasks/AssemblyDependency/Metadata/CorAssemblyFlags.cs).\n\n### Dual Guard Pattern\n\n```csharp\n#if FEATURE_WINDOWSINTEROP\n    if (IsWindows)\n    {\n        PInvoke.GetFileAttributesEx(fullPath, out WIN32_FILE_ATTRIBUTE_DATA data);\n    }\n#endif\n    // Cross-platform fallback\n```\n\n**WRONG**: `if (IsWindows) { #if FEATURE_WINDOWSINTEROP ... #endif }` — dead code in source builds.\n\nWindows-only files are excluded via `<Compile Remove>` instead — no `#if` inside needed.\n\n## Infrastructure\n\n**Define**: `src/Directory.BeforeCommon.targets` sets `FEATURE_WINDOWSINTEROP` + `$(FeatureWindowsInterop)` when `DotNetBuildSourceOnly != true`. Use `$(FeatureWindowsInterop)` in `.csproj` for `<Compile Remove>`/`<Compile Include>`.\n\n**CsWin32 config**: `src/Framework/NativeMethods.txt` (API list) + `NativeMethods.json` (`allowMarshaling: false`, `useSafeHandles: false`). Lives in Framework; other projects consume via `InternalsVisibleTo`. Do not add CsWin32 to other projects.\n\n**Guard selection**:\n\n| Guard | When | Runtime check? |\n|-------|------|----------------|\n| `#if FEATURE_WINDOWSINTEROP` | Multi-TFM Windows calls | Yes |\n| `#if FEATURE_WINDOWSINTEROP && NET` | Manual COM structs gated .NET-only (e.g. WMI). CsWin32-generated COM types via `ComScope<T>` work on net472 too — the generator emits `IComIID` on every TFM (see [cswin32-com](../cswin32-com/SKILL.md#icomiid-across-tfms)) | Yes |\n| `#if FEATURE_WINDOWSINTEROP && !NETSTANDARD` | CsWin32 types without `static abstract` (net472 + net10) | Yes |\n| `#if !NET` / `#if FEATURE_MSCOREE` | net472-only = inherently Windows | No |\n\n**Namespace imports** must be inside `#if FEATURE_WINDOWSINTEROP`. WDK APIs use `Windows.Wdk` namespace.\n\n**Files**: `src/Framework/Windows/` (CsWin32 partials), `src/Shared/Win32/` (COM helpers), `src/Framework/Utilities/Wmi/` (.NET-only COM structs).\n\n### Constant Replacements\n\n`NativeMethodsShared.S_OK` → `HRESULT.S_OK`, `InvalidHandle` → `HANDLE.INVALID_HANDLE_VALUE`, `FILE_ATTRIBUTE_DIRECTORY` → `FILE_FLAGS_AND_ATTRIBUTES.FILE_ATTRIBUTE_DIRECTORY`, `STD_OUTPUT_HANDLE` → `STD_HANDLE.STD_OUTPUT_HANDLE`, `GENERIC_READ` → `FILE_ACCESS_RIGHTS.FILE_GENERIC_READ`. Pattern: `CsWin32EnumType.ORIGINAL_NAME` — check generated types in `obj/`.\n\n**Always prefer the generated enum over a local copy.** Before defining `private const int ERROR_*` / `private enum FooFlags`, grep the CsWin32 metadata:\n\n- `ERROR_*` (Win32 error codes) → `WIN32_ERROR.ERROR_*` (uint enum)\n- `HRESULT` codes → `HRESULT.S_OK`, etc.\n- Restart Manager status/type → `RM_APP_STATUS` / `RM_APP_TYPE`\n- File flags → `FILE_FLAGS_AND_ATTRIBUTES`, `FILE_ACCESS_RIGHTS`, `FILE_SHARE_MODE`, `FILE_CREATION_DISPOSITION`\n- Process flags → `PROCESS_CREATION_FLAGS`, `STARTUPINFOW_FLAGS`, `PROCESS_ACCESS_RIGHTS`\n- Memory mapping → `PAGE_PROTECTION_FLAGS`, `FILE_MAP`\n- Shell folder → `KNOWN_FOLDER_FLAG`\n\nCast `int`/`uint` return codes via `(WIN32_ERROR)res` for `switch` and equality. Add the enum to `NativeMethods.txt` if not yet generated, then check `obj/.../generated/Microsoft.Windows.CsWin32/.../Windows.Win32.<EnumName>.g.cs`.\n\n**Some flag values are standalone constants, not enum members.** A Win32 `#define` outside a `typedef enum` generates as an `internal const` on `PInvoke`. Example: the `LoadTypeLibEx` flags are `PInvoke.LOAD_TLB_AS_32BIT` / `_64BIT` (`uint`), **not** members of `REGKIND` (which has only `REGKIND_DEFAULT/REGISTER/NONE`). Add the *constant name* to `NativeMethods.txt` like any API. OR it onto an enum at the constant's width and cast back: `(REGKIND)((uint)REGKIND.REGKIND_NONE | PInvoke.LOAD_TLB_AS_32BIT)`. Don't reintroduce a local `const` CsWin32 already emits.\n\n**Match local types to the CsWin32 type.** Instead of `int res = (int)PInvoke.RmStartSession(...)` and casting at every comparison, declare `WIN32_ERROR res = PInvoke.RmStartSession(...)` and let helpers like `GetException(WIN32_ERROR res, ...)` take the typed value. Cast to `int`/`uint` only at the boundary where a non-CsWin32 API needs it (e.g. `new Win32Exception((int)res, ...)`). The same applies to `HRESULT`, `BOOL`, `HANDLE`, `PROCESS_CREATION_FLAGS`, etc.\n\n**Delete local mirror enums** that exist solely to mirror the Win32 one. The generated CsWin32 type is the source of truth.\n\n### FILETIME Conversions\n\nUse the helpers in [`src/Framework/Windows/Win32/Foundation/FileTimeExtensions.cs`](../../../src/Framework/Windows/Win32/Foundation/FileTimeExtensions.cs):\n\n- `fileTime.ToLong()` → 64-bit ticks\n- `fileTime.ToDateTime()` → local `DateTime` (FILETIME values returned as local time, e.g. `RM_PROCESS_INFO.ProcessStartTime`)\n- `fileTime.ToDateTimeUtc()` → UTC `DateTime` (FILETIME values returned as UTC, e.g. `WIN32_FILE_ATTRIBUTE_DATA.ftLastWriteTime`)\n\nDo **not** hand-roll `DateTime.FromFileTime((long)hi << 32 | lo)` — use the helpers for consistency. Note CsWin32-generated structs use `ComTypes.FILETIME` (int fields) for COM members and `Windows.Win32.Foundation.FILETIME` (uint fields) for kernel ones; the extension covers `ComTypes.FILETIME`.\n\n## BufferScope<T>\n\n`BufferScope<T>` (`src/Framework/Utilities/BufferScope.cs`) — stackalloc initial buffer with `ArrayPool<T>` fallback. Lives in Framework, available to all projects via `InternalsVisibleTo`.\n\n```csharp\nusing BufferScope<char> buffer = new(stackalloc char[(int)PInvoke.MAX_PATH]);\nint length = (int)PInvoke.GetShortPathName(path, buffer.AsSpan());\nif (length > buffer.Length)\n{\n    buffer.EnsureCapacity(length);\n    length = (int)PInvoke.GetShortPathName(path, buffer.AsSpan());\n}\nif (length > 0) path = buffer.Slice(0, length).ToString();\n```\n\n- `ref struct` — always use with `using`. Never stack-allocate more than 1024 bytes.\n- Check CsWin32 convenience overloads (e.g. `GetShortPathName(string, Span<char>)`) before writing `fixed` blocks.\n\n## Gotchas\n\n### CA1416 Platform Compatibility\n\nNo blanket `NoWarn` — handle semantically:\n- `if (IsWindows)` satisfies `[SupportedOSPlatform]` — no pragma needed\n- `if (IsUnixLike)` satisfies `[UnsupportedOSPlatform(\"windows\")]`\n- **Never use `!IsWindows`** — use `else if (IsUnixLike)`. See `documentation/specs/CA1416-analyzer-analysis.md`\n- Use versioned `[SupportedOSPlatform(\"windows6.1\")]` on methods calling CsWin32 APIs\n- `#pragma warning disable CA1416` only for **static local functions** (analyzer limitation)\n- CS0592 prevents `[SupportedOSPlatform]` on `partial struct` — put on individual members instead\n\n### Type Conversions\n\n- `HANDLE ↔ IntPtr`: `(HANDLE)intPtr` / `(IntPtr)h.Value`. Sentinels: `HANDLE.Null`, `HANDLE.INVALID_HANDLE_VALUE`\n- `FILETIME` conversion: `data.ftLastWriteTime.ToLong()`, `.ToDateTime()` (local), `.ToDateTimeUtc()` — see \"FILETIME Conversions\" above. CsWin32 uses `ComTypes.FILETIME` (int fields), not `Win32.Foundation.FILETIME`\n- `SafeFileHandle`: `new SafeFileHandle((IntPtr)h.Value, true)`, pass with `(HANDLE)handle.DangerousGetHandle()`\n- Nullable structs: `(SECURITY_ATTRIBUTES?)null`\n- Enum flags: use bitwise `&` — `HasFlag()` boxes on .NET Framework\n- Anonymous unions: `systemInfo.Anonymous.Anonymous.wProcessorArchitecture` — check generated source in `obj/`\n\n### Source-Build Verification (REQUIRED before pushing)\n\nSource builds (`DotNetBuildSourceOnly=true`) disable `FEATURE_WINDOWSINTEROP`. CI treats **all warnings as errors**. Run both builds before every push:\n\n```shell\n# Normal build\ndotnet msbuild MSBuild.Dev.slnf -v:q\n\n# Source-build — catches unused usings/members/docs from #if guards\ndotnet msbuild MSBuild.SourceBuild.slnf /p:DotNetBuildSourceOnly=true -v:q\n```\n\n**Everything** only referenced inside `#if FEATURE_WINDOWSINTEROP` must also be guarded:\n- **IDE0005**: `using` directives — most common failure\n- **IDE0051/IDE0052**: Private members (methods, fields, including helpers like `StringToByteArray`, constants like `ERROR_SHARING_VIOLATION`)\n- **CA1823**: Unused private fields (e.g. constants only consumed inside the guard)\n- **CS1587**: XML doc comments (move inside `#if`, not before)\n\nThe same applies when adding **polyfills** in `src/Framework/Polyfills/` (e.g. `SpanExtensions`, `IndexOfAnyExcept`): polyfills usually live behind `#if !NET` (or similar TFM guards) but are still consumed from `#if FEATURE_WINDOWSINTEROP` code paths. Always run the source-build to confirm the polyfill, its callers, and any helper members compile cleanly when interop is disabled — a polyfill referenced only by Windows-only code will trip IDE0051/CA1823 in source-only builds.\n\n<!-- File: .github/skills/deploy-msbuild-to-vs/SKILL.md (Tokens: ~2304 | Category: anthropic-skill) -->\n---\nname: deploy-msbuild-to-vs\ndescription: Deploy locally-built MSBuild binaries into a Visual Studio installation for testing and debugging. Use when you need VS itself to use your local MSBuild changes, or when debugging MSBuild as invoked by VS.\nargument-hint: Deploy local MSBuild to VS, patch VS with built MSBuild, debug MSBuild inside VS.\n---\n\n# Deploying Built MSBuild into Visual Studio\n\nThis skill guides you through replacing Visual Studio's bundled MSBuild with your locally-built binaries, so that builds initiated from within VS use your changes. It also covers debugging MSBuild when invoked by VS and restoring the original state.\n\n## When to Use\n\n- You need Visual Studio's build (Ctrl+Shift+B, Build Solution, design-time builds) to use your local MSBuild changes.\n- You are debugging an issue that only reproduces when MSBuild is called through the VS process (`devenv.exe`).\n- You want to test MSBuild changes against a real VS solution end-to-end.\n\n**If you only need command-line testing**, prefer the bootstrap approach instead (see the `use-bootstrap-msbuild` skill). It is faster and does not risk breaking your VS installation.\n\n## Prerequisites\n\n- Windows (the deploy script targets VS on Windows).\n- The latest Visual Studio 2026 installed with the components listed in [`.vsconfig`](../../../.vsconfig).\n- .NET Framework 3.5 installed on the Windows machine.\n- An **administrator** PowerShell prompt (required to write into `Program Files`).\n- The MSBuild repo cloned and buildable.\n\n## Step 1: Build MSBuild\n\nRun a full build from the repo root to produce the bootstrap output:\n\n```powershell\n.\\build.cmd\n```\n\nBy default this builds `Debug` configuration. If you want `Release`:\n\n```powershell\n.\\build.cmd /p:Configuration=Release\n```\n\nThe build creates bootstrap binaries under `artifacts\\bin\\MSBuild.Bootstrap\\{configuration}\\`.\n\n## Step 2: Locate Your VS MSBuild Bin Folder\n\nFind the MSBuild `Bin` folder inside your VS installation. The path depends on your VS version and edition.\n\n**⚠ Important**: If you have multiple VS versions or editions installed side-by-side, make sure you identify the correct installation path for the instance you want to patch. Confirm the path exists before deploying.\n\nThe general pattern is:\n\n```text\nC:\\Program Files\\Microsoft Visual Studio\\{version}\\{edition}\\MSBuild\\Current\\Bin\n```\n\nFor Visual Studio 2026, `{version}` is `18` (not `2026`). Common `{edition}` folders include `Enterprise`, `Professional`, and `Community`; this is customizable, so use `vswhere` below to confirm the path.\n\nExample paths:\n\n| VS Version | Edition | Typical Path |\n| ---------- | ------- | ------------ |\n| 2026 | Enterprise | `C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin` |\n| 2026 | Community | `C:\\Program Files\\Microsoft Visual Studio\\18\\Community\\MSBuild\\Current\\Bin` |\n\n### Finding the path automatically\n\nThe most reliable way to locate your VS installation is with `vswhere`:\n\n```powershell\n# List all VS installations with their paths\n& \"${env:ProgramFiles(x86)}\\Microsoft Visual Studio\\Installer\\vswhere.exe\" -all -prerelease -format table\n\n# Get the MSBuild Bin path for the latest installation\n$vsPath = & \"${env:ProgramFiles(x86)}\\Microsoft Visual Studio\\Installer\\vswhere.exe\" -latest -prerelease -property installationPath\n\"$vsPath\\MSBuild\\Current\\Bin\"\n```\n\nAlternatively, from a VS Developer Command Prompt:\n\n```cmd\nwhere msbuild\n```\n\nUse the directory containing that `MSBuild.exe`.\n\n## Step 3: Deploy with the Script\n\nFrom an **administrator** PowerShell, run:\n\n```powershell\n.\\scripts\\Deploy-MSBuild.ps1 -destination \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\"\n```\n\n### Script Parameters\n\n| Parameter | Default | Description |\n| --------- | ------- | ----------- |\n| `-destination` | *(required)* | The VS MSBuild `Bin` folder path |\n| `-configuration` | `Debug` | Must match the configuration used in `build.cmd` |\n| `-runtime` | `Detect` | Auto-detects `Desktop` vs `Core` from the path. Use `Desktop` for VS, `Core` for .NET SDK |\n| `-binDirectory` | `artifacts\\bin` | Override if build output is elsewhere |\n| `-makeBackup` | `$true` | Creates a timestamped `Backup-*` folder before overwriting |\n\n### What Gets Copied\n\nThe script copies from `artifacts\\bin\\MSBuild.Bootstrap\\{configuration}\\net472\\`:\n\n- **Core DLLs**: `Microsoft.Build.dll`, `Microsoft.Build.Framework.dll`, `Microsoft.Build.Tasks.Core.dll`, `Microsoft.Build.Utilities.Core.dll`, `Microsoft.NET.StringTools.dll`\n- **Targets files**: `Microsoft.Common.targets`, `Microsoft.CSharp.targets`, `Microsoft.VisualBasic.targets`, and many more\n- **Executables**: `MSBuild.exe` (x86 and amd64), `MSBuildTaskHost.exe`\n- **Framework dependencies**: Various `System.*` assemblies needed for .NET Framework\n\n## Step 4: Test in Visual Studio\n\n1. **Close and reopen** Visual Studio (it caches MSBuild on startup).\n2. Open a solution and build — VS now uses your locally-built MSBuild.\n3. Verify by checking the build output or binary log for your changes.\n\n## Restoring the Original MSBuild\n\nThe deploy script creates a backup folder at `{destination}\\Backup-{timestamp}\\` by default.\n\nTo restore:\n\n```powershell\n# Find the backup folder\nGet-ChildItem \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\\Backup-*\"\n\n# Copy everything back\nCopy-Item \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\\Backup-{timestamp}\\*\" `\n          \"C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\\\" -Recurse -Force\n```\n\nAlternatively, run **Visual Studio Installer → Modify → Repair** to fully restore the original binaries.\n\n⚠ **CAUTION**: If you overwrite MSBuild in Visual Studio and something goes wrong, VS itself may break (since VS uses MSBuild internally). Always keep the backup and know how to restore before deploying.\n\n## Debugging MSBuild Inside Visual Studio\n\nWhen MSBuild is invoked by VS (for real builds or design-time builds), you can attach a debugger using environment variables. Set these **before** launching VS:\n\n### Breakpoint at BuildManager.BeginBuild (Recommended for VS)\n\n```powershell\n$env:MSBuildDebugBuildManagerOnStart = \"1\"   # 1 = Debugger.Launch(), 2 = wait for attach\n$env:MSBuildDebugProcessName = \"devenv\"      # Only trigger in the VS process\ndevenv.exe\n```\n\nThis breaks into `BuildManager.BeginBuild`, which is where VS calls into MSBuild's API.\n\n### Breakpoint at MSBuild Entry Point\n\n```powershell\n$env:MSBUILDDEBUGONSTART = \"1\"   # 1 = Launch debugger, 2 = Wait for attach, 3 = Main process only\ndevenv.exe\n```\n\n### Capturing Full Binary Logs from VS\n\nVS heavily filters MSBuild events and suppresses them entirely during design-time builds. To capture everything:\n\n```powershell\n$env:MSBuildDebugEngine = \"1\"\n$env:MSBUILDDEBUGPATH = \"C:\\temp\\MSBuild_Logs\"   # Optional: override log output directory\n$env:MSBuildDebugProcessName = \"devenv\"            # Optional: only log from VS\ndevenv.exe\n```\n\nThis injects a binary log at `BuildManager.BeginBuild`, capturing full logs for both real and design-time builds. Logs are saved to `MSBuild_Logs\\` under the current directory (or `MSBUILDDEBUGPATH` if set).\n\n## Deploying to .NET SDK Instead\n\nTo patch a .NET SDK installation rather than VS:\n\n```powershell\n.\\scripts\\Deploy-MSBuild.ps1 -destination \"C:\\Program Files\\dotnet\\sdk\\10.0.100\" -runtime Core\n```\n\nThe script auto-detects SDK paths (containing `dotnet` and `sdk`), but you can force it with `-runtime Core`.\n\n## Cross-Machine Deployment\n\nIf you cannot build MSBuild on the target machine:\n\n1. Build and deploy to an **empty folder**:\n\n   ```powershell\n   .\\scripts\\Deploy-MSBuild.ps1 -destination \"C:\\temp\\msbuild-deploy\"\n   ```\n\n2. Copy the contents of that folder to the target machine's VS MSBuild Bin folder.\n3. Make a manual backup of the target machine's files first.\n\n## Troubleshooting\n\n| Problem | Solution |\n| ------- | -------- |\n| Access denied when deploying | Run PowerShell as **Administrator** |\n| VS won't start after deploy | Restore from `Backup-*` folder or run VS Installer Repair |\n| Changes not reflected in VS | Close all VS instances and reopen; kill lingering `MSBuild.exe` processes |\n| Configuration mismatch | Ensure `-configuration` matches what you passed to `build.cmd` |\n| Build errors in MSBuild repo | Run `.\\build.cmd` from a clean state; see [Something's wrong in my build](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Something's-wrong-in-my-build.md) |\n\n## Quick Reference\n\n| Step | Command |\n| ---- | ------- |\n| Build MSBuild | `.\\build.cmd` |\n| Deploy to VS (admin) | `.\\scripts\\Deploy-MSBuild.ps1 -destination \"{VS MSBuild Bin path}\"` |\n| Deploy Release build | `.\\scripts\\Deploy-MSBuild.ps1 -destination \"{path}\" -configuration Release` |\n| Debug MSBuild in VS | Set `$env:MSBuildDebugBuildManagerOnStart = \"1\"` then launch `devenv.exe` |\n| Capture VS binlogs | Set `$env:MSBuildDebugEngine = \"1\"` then launch `devenv.exe` |\n| Restore original | Copy back from `Backup-*` folder or run VS Installer Repair |\n\n## See Also\n\n- [Deploy-MSBuild documentation](https://github.com/dotnet/msbuild/blob/main/documentation/Deploy-MSBuild.md)\n- [Building and Debugging on Full Framework](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md)\n- [Bootstrap Documentation](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Bootstrap.md)\n\n<!-- File: .github/skills/dotnet-aot-compat/references/polyfills.md (Tokens: ~385 | Category: anthropic-skill) -->\n# Polyfills for Older TFMs\n\n`DynamicallyAccessedMembersAttribute` shipped in .NET 5. For projects targeting netstandard2.0 or net472, you need a polyfill. The trimmer recognizes the attribute by name, so a local copy works:\n\n```csharp\n#if !NET\nnamespace System.Diagnostics.CodeAnalysis\n{\n    [AttributeUsage(AttributeTargets.Field | AttributeTargets.ReturnValue |\n                    AttributeTargets.GenericParameter | AttributeTargets.Parameter |\n                    AttributeTargets.Property, Inherited = false)]\n    internal sealed class DynamicallyAccessedMembersAttribute : Attribute\n    {\n        public DynamicallyAccessedMembersAttribute(DynamicallyAccessedMemberTypes memberTypes)\n            => MemberTypes = memberTypes;\n        public DynamicallyAccessedMemberTypes MemberTypes { get; }\n    }\n\n    [Flags]\n    internal enum DynamicallyAccessedMemberTypes\n    {\n        None = 0,\n        PublicParameterlessConstructor = 0x0001,\n        PublicConstructors = 0x0002 | PublicParameterlessConstructor,\n        NonPublicConstructors = 0x0004,\n        PublicMethods = 0x0008,\n        NonPublicMethods = 0x0010,\n        PublicFields = 0x0020,\n        NonPublicFields = 0x0040,\n        PublicNestedTypes = 0x0080,\n        NonPublicNestedTypes = 0x0100,\n        PublicProperties = 0x0200,\n        NonPublicProperties = 0x0400,\n        PublicEvents = 0x0800,\n        NonPublicEvents = 0x1000,\n        Interfaces = 0x2000,\n        All = ~None // Discouraged — prefer specific flags\n    }\n}\n#endif\n```\n\nSimilarly for `RequiresUnreferencedCodeAttribute` and `UnconditionalSuppressMessageAttribute` if needed on older TFMs.\n\n<!-- File: .github/skills/dotnet-aot-compat/SKILL.md (Tokens: ~3905 | Category: anthropic-skill) -->\n---\nname: dotnet-aot-compat\ndescription: >\n  Make .NET projects compatible with Native AOT and trimming by systematically\n  resolving IL trim/AOT analyzer warnings. USE FOR: making projects AOT-compatible,\n  fixing trimming warnings, resolving IL warnings (IL2026, IL2070, IL2067, IL2072,\n  IL3050), adding DynamicallyAccessedMembers annotations, enabling IsAotCompatible.\n  DO NOT USE FOR: publishing native AOT binaries, optimizing binary size, replacing\n  reflection-heavy libraries with alternatives.\n  INVOKES: no tools — pure knowledge skill.\nlicense: MIT\n---\n\n# dotnet-aot-compat\n\nMake .NET projects compatible with Native AOT and trimming by systematically resolving all IL trim/AOT analyzer warnings.\n\n## When to Use This Skill\n\n- **\"Make this project AOT-compatible\"**\n- **\"Fix trimming warnings\"** or **\"fix IL warnings\"**\n- **\"Resolve IL2070 / IL2067 / IL2072 / IL2026 / IL3050 warnings\"**\n- **\"Add DynamicallyAccessedMembers annotations\"**\n- **\"Enable IsAotCompatible in my .csproj\"**\n- **\"My project has trim analyzer warnings after upgrading to net8.0\"**\n- **\"Annotate reflection code for the trimmer\"**\n\n## When Not to Use This Skill\n\nDo not use this skill when the project exclusively targets .NET Framework (net4x), which does not support the trim/AOT analyzers.\n\n## Prerequisites\n\nAn existing .NET project targeting net8.0 or later (or multi-targeting with at least one net8.0+ TFM) and the corresponding .NET SDK installed.\n\n## Background: What AOT Compatibility Means\n\nNative AOT and the IL trimmer perform static analysis to determine what code is reachable. Reflection can break this analysis because the trimmer can't see what types/members are accessed at runtime. The `IsAotCompatible` property enables analyzers that flag these issues as build warnings (ILXXXX codes).\n\n## Critical Rules\n\n### ❌ Never suppress warnings incorrectly\n\n- **NEVER** use `#pragma warning disable` for IL warnings. It hides warnings from the Roslyn analyzer at build time, but the IL linker and AOT compiler still see the issue. The code will fail at trim/publish time.\n- **NEVER** use `[UnconditionalSuppressMessage]`. It tells both the analyzer AND the linker to ignore the warning, meaning the trimmer cannot verify safety. Raising an error at build time is always preferable to hiding the issue and having it silently break at runtime.\n\n### 💡 Preferred approaches\n\n- **Prefer** `[DynamicallyAccessedMembers]` annotations to flow type information through the call chain.\n- **Prefer** refactoring to eliminate patterns that break annotation flow (e.g., boxing `Type` through `object[]`).\n- **Use** `[RequiresUnreferencedCode]` / `[RequiresDynamicCode]` / `[RequiresAssemblyFiles]` to mark methods as fundamentally incompatible with trimming, propagating the requirement to callers. This surfaces the issue clearly rather than hiding it — callers must explicitly acknowledge the incompatibility.\n\n### Annotation flow is key\n\nThe trimmer tracks `[DynamicallyAccessedMembers]` annotations through assignments, parameter passing, and return values. If this flow is broken (e.g., by boxing a `Type` into `object`, storing in an untyped collection, or casting through interfaces), the trimmer loses track and warns. The fix is to preserve the flow, not suppress the warning.\n\n## Step-by-Step Procedure\n\n> **Do not explore the codebase up-front.** The build warnings tell you exactly which files and lines need changes. Follow a tight loop: **build → pick a warning → open that file at that line → apply the fix recipe → rebuild**. Reading or analyzing source files beyond what a specific warning points you to is wasted effort and leads to timeouts. Let the compiler guide you.\n>\n> ❌ Do NOT run `find`, `ls`, or `grep` to understand the project structure before building. Do NOT read README, docs, or architecture files. Your first action should be Step 1 (enable AOT analysis), then build.\n\n### Step 1: Enable AOT analysis in the .csproj\n\nAdd `IsAotCompatible`. If the project doesn't exclusively target net8.0+, add a TFM condition (AOT analysis requires net8.0+):\n\n```xml\n<PropertyGroup>\n  <IsAotCompatible Condition=\"$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))\">true</IsAotCompatible>\n</PropertyGroup>\n```\n\nThis automatically sets `EnableTrimAnalyzer=true` and `EnableAotAnalyzer=true` for compatible TFMs. For multi-targeting projects (e.g., `netstandard2.0;net8.0`), the condition ensures no `NETSDK1210` warnings on older TFMs.\n\n### Step 2: Build and collect warnings\n\n```bash\ndotnet build <project.csproj> -f <net8.0-or-later-tfm> --no-incremental 2>&1 | grep 'IL[0-9]\\{4\\}'\n```\n\nSort and deduplicate. Common warning codes:\n- **IL2070**: Reflection call on a `Type` parameter missing `[DynamicallyAccessedMembers]`\n- **IL2067**: Passing an unannotated `Type` to a method expecting `[DynamicallyAccessedMembers]`\n- **IL2072**: Return value or extracted value missing annotation (often from unboxing)\n- **IL2057**: `Type.GetType(string)` with a non-constant argument\n- **IL2026**: Calling a method marked `[RequiresUnreferencedCode]`\n- **IL2050**: P/invoke method with COM marshalling parameters\n- **IL2075**: Return value flows into reflection without annotation\n- **IL2091**: Generic argument missing `[DynamicallyAccessedMembers]` required by constraint\n- **IL3000**: `Assembly.Location` returns empty string in single-file/AOT apps\n- **IL3050**: Calling a method marked `[RequiresDynamicCode]`\n\n### Step 3: Triage warnings by code (do NOT read every file)\n\nGroup the warnings from Step 2 by warning code and count them. **Do not open individual files yet.** Identify the top 1-2 patterns by count — these drive your fix strategy:\n\n| Pattern | Typical fix |\n|---------|-------------|\n| Many IL2026 + IL3050 from `JsonSerializer` | **Go to Strategy C immediately** — create a `JsonSerializerContext`, then batch-update all call sites |\n| IL2070/IL2087 on `Type` parameters | Add `[DynamicallyAccessedMembers]` to the innermost method, then cascade outward |\n| IL2067 passing unannotated `Type` | Annotate the parameter at the source |\n\n**In most real projects, IL2026/IL3050 from JsonSerializer dominate.** Start with Strategy C unless the warning breakdown clearly shows otherwise. After the batch JSON fix, handle remaining warnings with Strategies A–B. Only use Strategy D as a last resort.\n\n### Step 4: Fix warnings iteratively (innermost first)\n\nWork from the **innermost** reflection call outward. Each fix may cascade new warnings to callers.\n\n**Stay warning-driven.** For each warning, open only the file and line the compiler reported, identify the pattern, apply the matching fix recipe below, and move on. Do not scan the codebase for similar patterns or try to understand the full architecture — fix what the compiler tells you, rebuild, and let new warnings guide the next change. Fix a small batch of warnings (5-10), then rebuild immediately to check progress.\n\n**Use sub-agents when available.** If you can launch sub-agents (e.g., via a `task` tool), dispatch **multiple sub-agents in parallel** to edit different files simultaneously. Keep the main loop focused on building, parsing warnings, and dispatching — delegate actual file edits to sub-agents. For batch JSON updates, give each sub-agent 5-10 files to update in one prompt. **After 2 build-fix cycles, dispatch all remaining file edits to sub-agents in parallel — do not continue fixing files sequentially.** Example:\n\n> Update these files to use source-generated JSON: `src/Models/Resource.Serialization.cs`, `src/Models/Identity.Serialization.cs`, `src/Models/Plan.Serialization.cs`. In each file, replace `JsonSerializer.Serialize(writer, value)` with `JsonSerializer.Serialize(writer, value, MyProjectJsonContext.Default.TypeName)` and `JsonSerializer.Deserialize<T>(ref reader)` with `JsonSerializer.Deserialize(ref reader, MyProjectJsonContext.Default.TypeName)`. Only edit the JsonSerializer call sites.\n\n#### Strategy A: Add `[DynamicallyAccessedMembers]` (preferred)\n\nWhen a method uses reflection on a `Type` parameter, annotate the parameter to tell the trimmer what members are needed:\n\n```csharp\nusing System.Diagnostics.CodeAnalysis;\n\n// Before (warns IL2070):\nvoid Process(Type t) {\n    var method = t.GetMethod(\"Foo\");  // trimmer can't verify\n}\n\n// After (clean):\nvoid Process([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type t) {\n    var method = t.GetMethod(\"Foo\");  // trimmer preserves public methods\n}\n```\n\nWhen you annotate a parameter, **all callers** must now pass properly annotated types. This cascades outward — follow each caller and annotate or refactor as needed. **The caller's annotation must include at least the same member types as the callee's.** If the callee requires `PublicConstructors | NonPublicConstructors`, the caller must specify the same or a superset — using only `NonPublicConstructors` will produce IL2091.\n\n#### Strategy B: Refactor to preserve annotation flow\n\nWhen annotation flow is broken by boxing (storing `Type` in `object`, `object[]`, or untyped collections), **refactor** to pass the `Type` directly:\n\n```csharp\n// BROKEN: Type boxed into object[], annotation lost\nvoid Process(object[] args) {\n    Type t = (Type)args[0];  // IL2072: annotation lost through boxing\n    Evaluate(t, ...);\n}\n\n// FIXED: Pass Type as a separate, annotated parameter\nvoid Process(\n    object[] args,\n    [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type calleeType,\n    ...) {\n    Evaluate(calleeType, ...);  // annotation flows cleanly\n}\n```\n\nCommon patterns that break flow and how to fix them:\n- **`object[]` parameter bags**: Extract the `Type` into a dedicated annotated parameter\n- **Dictionary/List storage**: Use a typed field with annotation instead\n- **Interface indirection**: Add annotation to the interface method's parameter\n- **Property with boxing getter**: Annotate the property's return type\n\n#### Strategy C: Source-generated JSON serialization (batch fix)\n\nWhen most warnings are IL2026/IL3050 from `JsonSerializer.Serialize`/`Deserialize`, this is a single mechanical fix applied in bulk:\n\n1. **Collect affected types** — grep for all `JsonSerializer.Serialize` and `JsonSerializer.Deserialize` call sites. Extract the type being serialized (the `<T>` in `Deserialize<T>`, or the runtime type of the object in `Serialize`).\n\n2. **Create one `JsonSerializerContext`** with `[JsonSerializable]` for every type found. **Skip types from external packages** (e.g., `ResponseError` from `Azure.Core`) — they won't source-generate for types you don't own. Handle external types separately via Gotcha #1 below.\n\n```csharp\n[JsonSerializerContext]\n[JsonSerializable(typeof(ManagedServiceIdentity))]\n[JsonSerializable(typeof(SystemData))]\n// ... one attribute per type YOU OWN\n// Do NOT add types from external packages (e.g., ResponseError)\ninternal partial class MyProjectJsonContext : JsonSerializerContext { }\n```\n\n3. **Batch-update all call sites** — do not read each file individually. Apply the pattern mechanically:\n   - `JsonSerializer.Serialize(obj)` → `JsonSerializer.Serialize(obj, MyProjectJsonContext.Default.TypeName)`\n   - `JsonSerializer.Deserialize<T>(json)` → `JsonSerializer.Deserialize(json, MyProjectJsonContext.Default.TypeName)`\n\n   Find and update all call sites in one pass:\n   ```bash\n   # Find all files with JsonSerializer calls\n   grep -rl 'JsonSerializer\\.\\(Serialize\\|Deserialize\\)' src/ --include='*.cs'\n   ```\n   Then use sequential `edit` calls to apply the same transformation to every matching file. **Do not use `sed` for C# code** — generics like `Deserialize<T>()` have angle brackets and nested parentheses that sed will mangle.\n\n4. **Build once** to verify. Remaining warnings will be non-serialization issues — handle those with Strategies A–B or D.\n\n#### Strategy D: `[RequiresUnreferencedCode]` (last resort)\n\nWhen a method fundamentally requires arbitrary reflection that cannot be statically described:\n\n```csharp\n[RequiresUnreferencedCode(\"Loads plugins by name using Assembly.Load\")]\npublic void LoadPlugin(string assemblyName) {\n    var asm = Assembly.Load(assemblyName);\n    // ...\n}\n```\n\nThis propagates to callers — they must also be annotated with `[RequiresUnreferencedCode]`. Use sparingly; it marks the entire call chain as trim-incompatible.\n\n### Step 5: Rebuild and repeat\n\nAfter each small batch of fixes (5-10 warnings), rebuild with `--no-incremental` and check for new warnings. **Do not attempt to fix all warnings before rebuilding** — frequent rebuilds catch mistakes early and reveal cascading warnings. Fixes cascade — annotating an inner method may surface warnings in its callers. Repeat until `0 Warning(s)`.\n\n### Step 6: Validate all TFMs\n\nBuild all target frameworks to ensure:\n- **0 IL warnings** on net8.0+ TFMs\n- **No NETSDK1210 warnings** (the `IsAotCompatible` condition handles this)\n- **Clean builds** on older TFMs (netstandard2.0, net472, etc.)\n\n```bash\ndotnet build <project.csproj>  # builds all TFMs\n```\n\n## Stop Signals\n\n- **Do not analyze more than 2-3 representative files per warning pattern.** After identifying the fix for a pattern, apply it to all matching files without reading each one first.\n- **Start fixing after one build.** Do not do a second analysis pass — begin implementing fixes for the most common warning pattern immediately after Step 3 triage.\n- Stop after achieving **0 IL warnings** for net8.0+ TFMs. Don't optimize or refactor already-clean annotations.\n- If a warning requires **architectural refactoring** beyond annotation flow fixes (e.g., replacing an entire serialization layer), document it and stop — don't rewrite large subsystems.\n- Limit to **3 build-fix iterations** per warning. If annotation flow doesn't resolve it after 3 attempts, escalate to `[RequiresUnreferencedCode]`.\n- Don't chase warnings in **third-party dependencies** you can't modify. Note them and move on.\n- If the user asked a scoped question (e.g., \"fix warnings in this file\"), don't expand to the entire project.\n\n## Polyfills for Older TFMs\n\nFor multi-targeting projects that include netstandard2.0 or net472, you need polyfills for `DynamicallyAccessedMembersAttribute` and related types. See [references/polyfills.md](references/polyfills.md).\n\n## Common Gotchas\n\n1. **External types without AOT-safe serialization**: When a type comes from a dependency you can't modify (e.g., `ResponseError` from `Azure.Core`) and it lacks a source-generated serializer, `Options.GetConverter<T>()` is reflection-based and will produce IL warnings. First check if the type implements `IJsonModel<T>` (common in Azure SDK) — if so, bypass `JsonSerializer` entirely:\n\n```csharp\n// Before (IL2026 — JsonSerializer uses reflection):\nJsonSerializer.Serialize(writer, errorValue);\n\n// After (AOT-safe — uses IJsonModel directly):\n((IJsonModel<ResponseError>)errorValue).Write(writer, ModelReaderWriterOptions.Json);\n\n// For deserialization:\nvar error = ((IJsonModel<ResponseError>)new ResponseError()).Create(ref reader, ModelReaderWriterOptions.Json);\n```\n\nDo **not** add the external type to your `JsonSerializerContext` — it won't source-generate for types you don't own. If the type doesn't implement `IJsonModel<T>`, write a custom `JsonConverter<T>` with manual `Utf8JsonReader`/`Utf8JsonWriter` logic and register it via `[JsonSourceGenerationOptions]` on your context.\n\n2. **Serialization libraries**: Most reflection-based serializers (e.g., `Newtonsoft.Json`, `XmlSerializer`) are not AOT-compatible. Migrate to a source-generation-based serializer such as `System.Text.Json` with a `JsonSerializerContext`. If migration is not feasible, mark the serialization call site with `[RequiresUnreferencedCode]`.\n\n3. **Shared projects / projitems**: When source is shared between multiple projects via `<Import>`, annotations added to shared code affect ALL consuming projects. Verify that all consumers still build cleanly.\n\n## References\n\n[Limitations](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/?tabs=windows%2Cnet8#limitations-of-native-aot-deployment)\n[Conceptual: Understanding trimming](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-concepts)\n[How-to: trim compat](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/fixing-warnings)\n\n## Checklist\n\n- [ ] Added `<IsAotCompatible>` with TFM condition to .csproj\n- [ ] Built with AOT analyzers enabled (net8.0+ TFM)\n- [ ] Fixed all IL warnings via annotations or refactoring\n- [ ] No `#pragma warning disable` or `[UnconditionalSuppressMessage]` used for any IL warning\n- [ ] Polyfills present for older TFMs if needed\n- [ ] All target frameworks build with 0 warnings\n- [ ] Verified shared/linked source doesn't break sibling projects\n\n<!-- File: .github/skills/integrating-sdk-and-msbuild/SKILL.md (Tokens: ~1250 | Category: anthropic-skill) -->\n---\nname: integrating-sdk-and-msbuild\ndescription: 'Guides work on the SDK-MSBuild integration boundary. Consult when authoring or modifying SDK targets, working on dotnet CLI to MSBuild invocation, handling project-reference protocol, coordinating cross-repo changes with dotnet/sdk, debugging property resolution or import ordering, working on restore/build/publish/pack target chains, or dealing with Directory.Build.props/targets interaction.'\nargument-hint: 'Describe the SDK integration scenario or cross-repo coordination need.'\n---\n\n# SDK-MSBuild Integration Patterns\n\nMSBuild operates as a component within the .NET SDK. This boundary is the most complex integration point in the .NET build stack, spanning MSBuild (engine), SDK (target implementations), NuGet (restore), and Roslyn (compilation).\n\n## The Evaluation Boundary\n\nUnderstanding MSBuild's evaluation order is critical for SDK target authoring:\n\n```\n1. Environment variables\n2. Global properties (from CLI: -p:Foo=Bar)\n3. Project-level properties (file order, with imports):\n   ┌─ Sdk.props (SDK defaults)\n   ├─ Directory.Build.props (user overrides BEFORE project)\n   ├─ <Project> properties (the .csproj itself)\n   ├─ Directory.Build.targets (user overrides AFTER project)\n   └─ Sdk.targets (SDK target definitions)\n4. Item definitions\n5. Items (including SDK default globs)\n```\n\n### Key Import Order Rules\n\n- **SDK props import BEFORE user project** — SDK defaults can be overridden by the user\n- **SDK targets import AFTER user project** — SDK targets see user-specified properties\n- **`Directory.Build.props` is imported from `Microsoft.Common.props` as an early user extension point after core defaults are computed** — use it for solution-wide customization\n- **Property defaults set in SDK must not override user-specified values** — always use `Condition=\"'$(Prop)' == ''\"`\n\n```xml\n<!-- CORRECT: SDK default that respects user override -->\n<OutputType Condition=\"'$(OutputType)' == ''\">Library</OutputType>\n\n<!-- WRONG: Unconditional set clobbers user's .csproj -->\n<OutputType>Library</OutputType>\n```\n\n## Restore and Build Separation\n\n**Restore and Build must never run in the same evaluation.** The restore phase generates `.g.props` and `.g.targets` files that must be imported during evaluation — but they don't exist until restore completes.\n\n- `dotnet build` implicitly runs restore then build as **separate invocations**\n- `dotnet build --no-restore` skips restore, assuming it already happened\n- Running both targets in one invocation (`/t:Restore;Build`) is a known anti-pattern that causes intermittent failures\n\n## Project Reference Protocol\n\nThe project-reference protocol spans MSBuild, SDK, and NuGet. It is the most complex integration boundary.\n\n### How It Works\n\n1. Outer build dispatches to `_GetProjectReferenceTargetFrameworkProperties` to determine inner build parameters\n2. Inner build runs with the resolved `TargetFramework` (singular) for each referenced project\n3. `GetTargetPath` returns the output assembly for the referencing project to consume\n\n### Rules\n\n- Protocol changes must be coordinated across MSBuild, SDK, and NuGet teams\n- Multi-targeting projects (`<TargetFrameworks>`) dispatch multiple inner builds\n- The outer build must not assume a single target framework\n- `SetTargetFramework` is how the outer build communicates the chosen framework to inner builds\n\n## Target Authoring in SDK Context\n\n### Extension Points\n\nSDK provides well-known extension points for targets:\n\n| Extension Point | Use For |\n|----------------|---------|\n| `$(BuildDependsOn)` | Adding to the Build chain |\n| `$(CompileDependsOn)` | Pre-compilation steps |\n| `$(PublishDependsOn)` | Publish pipeline additions |\n| `$(PackDependsOn)` | NuGet pack pipeline additions |\n| `BeforeTargets=\"Build\"` | Use sparingly; prefer `DependsOnTargets` |\n\n### Ordering Rules\n\n1. **Use `DependsOnTargets` for required predecessors** — it's explicit and predictable\n2. **`BeforeTargets`/`AfterTargets` should be used sparingly** — they create implicit ordering that's hard to debug\n3. **Incremental build targets need precise `Inputs` and `Outputs`** — incorrect declarations cause either rebuild-every-time or stale-output bugs\n4. **Test with multi-targeting** — target chains execute once per `TargetFramework` in the inner build\n\n## Cross-Repo Coordination\n\nChanges that touch the MSBuild-SDK boundary often require coordinated PRs:\n\n1. **MSBuild engine change** → may need SDK target updates\n2. **SDK target change** → may need MSBuild API additions\n3. **NuGet restore change** → affects both MSBuild evaluation and SDK targets\n\n### Coordination Protocol\n\n- File an issue in both repos describing the cross-cutting change\n- Land the MSBuild change first (lower in the stack)\n- Update SDK to consume the new MSBuild via dependency flow\n- Test end-to-end with the SDK's MSBuild integration tests\n\n## Design-Time Builds\n\nVisual Studio uses design-time builds with different target contracts:\n\n- Design-time builds call `ResolveProjectReferences` but not `Build`\n- They set `$(DesignTimeBuild)=true` and `$(BuildingProject)=false`\n- Targets that should not run during design-time must check these properties\n- Design-time builds must be fast — avoid expensive I/O or compilation\n\n## Common Integration Bugs\n\n| Symptom | Likely Cause |\n|---------|-------------|\n| Property has wrong value | Import ordering — check if SDK prop overrides user setting |\n| Target runs in wrong order | Missing `DependsOnTargets` declaration |\n| Build works, restore fails | Evaluation-time dependency on restore-generated files |\n| Works single-target, fails multi-target | Target assumes single `$(TargetFramework)` |\n| CLI build works, VS build fails | Design-time build target contract violation |\n\n<!-- File: .github/skills/maintaining-binary-log-compatibility/SKILL.md (Tokens: ~1293 | Category: anthropic-skill) -->\n---\nname: maintaining-binary-log-compatibility\ndescription: 'Guides changes to MSBuild binary log infrastructure. Consult when modifying BinaryLogger or BinaryLogReplayEventSource, adding new BuildEventArgs types, changing event serialization/deserialization, modifying ProjectImportsCollector, adjusting message importance levels, or making changes that affect .binlog content. Also applies when verifying that behavioral changes are properly reflected in binary log output.'\nargument-hint: 'Describe the binary log change or event serialization concern.'\n---\n\n# Binary Log Considerations\n\nThe binary log (`.binlog`) is MSBuild's primary diagnostic format and source of truth for build analysis. It captures the complete build event stream with full fidelity. Format changes have strict compatibility requirements.\n\nFor general binary log usage, see [Binary-Log.md](../../../documentation/wiki/Binary-Log.md).\n\n## Core Principles\n\n1. **The binlog captures everything.** All meaningful build events must be represented. If your change alters build behavior, it must be observable in the binlog.\n2. **Format changes must be backward-compatible.** Older versions of MSBuild Structured Log Viewer and other tools must be able to read binlogs produced by newer MSBuild, at minimum gracefully degrading.\n3. **Forward compatibility matters too.** Newer viewers should handle binlogs from older MSBuild without crashing, even if some events are unrecognized.\n\n## Architecture Overview\n\n```\nBuild Engine\n  → BuildEventArgs (structured events)\n    → BinaryLogger (serializes to .binlog)\n      → ProjectImportsCollector (embeds project/targets files)\n\nReplay:\n  .binlog file\n    → BinaryLogReplayEventSource (deserializes)\n      → Any ILogger (console, structured log viewer, analyzers)\n```\n\nKey source files:\n- `src/Build/Logging/BinaryLogger/BinaryLogger.cs` — the logger itself\n- `src/Framework/BuildEventArgs.cs` — base class for all build events\n- `src/Build/Logging/BinaryLogger/ProjectImportsCollector.cs` — captures imported files\n\n## Adding New Build Event Types\n\nWhen adding a new `BuildEventArgs` subclass:\n\n1. **Define the new event class** inheriting from the appropriate base (`BuildMessageEventArgs`, `BuildWarningEventArgs`, etc.)\n2. **Add serialization and forwarding support** — implement `WriteToStream` and `CreateFromStream`, then trace the event through every dispatcher and transport (including task-host forwarding) without making unknown event types fatal\n3. **Increment the binary log version** if the new event type changes the format\n4. **Add a new record type constant** in the binary logger's record type enum\n5. **Handle the unknown-type case** in the replay source — older readers must skip gracefully\n\n### Serialization Compatibility Rules\n\n- **Never remove fields** from existing event args serialization\n- **New fields must be appended** to the end of the serialization stream\n- **Use version checks** when reading — if the binlog version is older, use defaults for new fields\n- **Nullable fields** should serialize a presence flag before the value\n\n```csharp\n// Pattern for backward-compatible field addition\nif (logVersion >= newFieldVersion)\n{\n    writer.Write(newField);\n}\n\n// Reading with backward compatibility\nif (logVersion >= newFieldVersion)\n{\n    newField = reader.ReadString();\n}\nelse\n{\n    newField = defaultValue;\n}\n```\n\n## Message Importance Levels\n\nImportance controls what appears in console output, but **everything goes to binlog** regardless of importance.\n\n| Level | Use For | Console Verbosity |\n|-------|---------|------------------|\n| `High` | Critical user-facing information | Minimal and above |\n| `Normal` | Standard build progress | Normal and above |\n| `Low` | Detailed diagnostic information | Detailed and above |\n| `Diagnostic` | Internal debugging | Diagnostic only |\n\n### Rules\n\n- Default to `Normal` for user-relevant information\n- Use `Low` for information useful when debugging but noisy in normal builds\n- `High` is reserved for important warnings/status — use sparingly\n- Never skip logging because \"it's too verbose\" — log at `Low` instead\n\n## ProjectImportsCollector\n\nThe `ProjectImportsCollector` embeds all imported `.props`, `.targets`, and project files into the binlog. This enables the \"preprocessed view\" in log viewers.\n\n### Considerations\n\n- `MSBUILDLOGIMPORTS=1` (or the `/bl` switch) enables import collection\n- Imported file content is captured at evaluation time — reflects the actual content used\n- Large import chains increase binlog size — this is acceptable for diagnostic completeness\n- Sensitive content in imported files will be embedded — document this for users\n\n## Changes That Affect Binlog Content\n\nWhen modifying MSBuild behavior, verify binlog impact:\n\n| Change Type | Binlog Consideration |\n|------------|---------------------|\n| New property set during evaluation | Appears in `PropertyInitialValue` or `PropertyReassignment` events |\n| New target added | Produces `TargetStarted`/`TargetFinished` events |\n| Changed task behavior | Task output items/properties captured in `TaskFinished` |\n| New warning/error | Captured as `BuildWarningEventArgs`/`BuildErrorEventArgs` |\n| Modified import chain | Changes which files `ProjectImportsCollector` captures |\n\n## Testing Binlog Changes\n\n- **Round-trip test**: Write a binlog, replay it, verify all events are reconstructed\n- **Version compatibility test**: Verify older replay sources handle new events gracefully\n- **Content verification**: Assert specific events appear in the binlog for behavioral changes\n- **Size regression**: Monitor binlog size for unexpectedly large increases\n- Use `BinaryLogReplayEventSource` in tests to verify binlog content programmatically\n\n## Checklist\n\n- [ ] New event types have `WriteToStream`/`CreateFromStream` implementations\n- [ ] Binary log format version incremented if format changed\n- [ ] Backward compatibility: older readers skip/degrade gracefully\n- [ ] Forward compatibility: newer readers handle old format\n- [ ] Importance levels set correctly for new messages\n- [ ] Behavioral changes produce observable binlog events\n- [ ] Round-trip test passes (serialize → deserialize → verify)\n\n<!-- File: .github/skills/merge-dependency-updates/SKILL.md (Tokens: ~2052 | Category: anthropic-skill) -->\n---\nname: merge-dependency-updates\ndescription: 'Review and merge open bot PRs: dependency updates from dotnet-maestro, codeflow from dotnet/dotnet, and OneLoc localization PRs. Produces a clickable dashboard with CI status, review state, and suspicious file flags. Use when you want to triage all open bot PRs in one pass.'\nargument-hint: 'Triage, update, and version-bump open bot PRs.'\n---\n\n# Managing Bot PRs (Dependencies, Codeflow, OneLoc)\n\nThis skill triages all open bot PRs in the MSBuild repo, produces a review dashboard, then optionally updates branches and bumps versions for merge readiness.\n\n## When to Use\n\n- You want to see the status of all open bot PRs at a glance\n- There are open dependency update / codeflow / OneLoc PRs that need merging\n- You want to process all of them in one pass with a clickable summary\n\n## PR Categories\n\n| Category | Author | Title Pattern | Example |\n|----------|--------|---------------|---------|\n| Dependency update | `dotnet-maestro[bot]` | `[<branch>] Update dependencies from dotnet/<repo>` | `[vs18.3] Update dependencies from dotnet/arcade` |\n| Codeflow | `dotnet-maestro[bot]` | `[<branch>] Source code updates from dotnet/dotnet` | `[vs18.3] Source code updates from dotnet/dotnet` |\n| OneLoc | `dotnet-bot` | `Localized file check-in by OneLocBuild Task: ...` | `Localized file check-in by OneLocBuild Task: Build definition ID 9434: Build ID ...` |\n\n## Step-by-Step Procedure\n\n### Step 1: Gather All Bot PRs\n\nRun **two searches in parallel** to find all open bot PRs:\n\n1. **Dependency updates + Codeflow:**\n   ```\n   Search: is:open author:app/dotnet-maestro repo:dotnet/msbuild\n   ```\n\n2. **OneLoc localization:**\n   ```\n   Search: is:open author:dotnet-bot repo:dotnet/msbuild (\"OneLocBuild\" OR \"Localized file check-in\")\n   ```\n\n### Step 2: Analyze Each PR (in parallel)\n\nFor each PR, query the following data using `gh api graphql`. Process PRs in parallel batches for speed.\n\n```bash\ngh api graphql -f query='\n  query {\n    repository(owner: \"dotnet\", name: \"msbuild\") {\n      pullRequest(number: <PR_NUMBER>) {\n        id\n        title\n        baseRefName\n        headRefName\n        mergeable\n        reviewDecision\n        commits(last: 1) {\n          nodes {\n            commit {\n              statusCheckRollup {\n                state\n              }\n            }\n          }\n        }\n        reviews(last: 10) {\n          nodes {\n            state\n            author { login }\n          }\n        }\n        reviewThreads(first: 20) {\n          nodes {\n            isResolved\n            comments(first: 1) {\n              nodes { body author { login } }\n            }\n          }\n        }\n        files(first: 50) {\n          nodes { path additions deletions }\n          totalCount\n        }\n      }\n    }\n  }\n'\n```\n\nFrom the response, extract:\n- **CI status**: from `statusCheckRollup.state` — `SUCCESS`, `FAILURE`, `PENDING`, `ERROR`, `EXPECTED`, or `null`. Map `ERROR` to failure (❌) and `EXPECTED` to neutral (⚪) in the dashboard.\n- **Review decision**: `APPROVED`, `REVIEW_REQUIRED`, `CHANGES_REQUESTED`\n- **Approvals**: count and list of `APPROVED` reviews\n- **Review comments**: any unresolved threads from the first 20 results (especially `CHANGES_REQUESTED`)\n- **Files changed**: use `totalCount` for the file count; use the first 50 file paths from `files.nodes` for suspicious file detection\n- **Suspicious flag**: check if any file paths are outside expected patterns (see below)\n\n#### Suspicious File Detection\n\n| PR Category | Expected Files | Flag if... |\n|-------------|---------------|------------|\n| Dependency update (arcade) | `eng/**`, `global.json` | Any file outside `eng/` or `global.json` |\n| Dependency update (roslyn/nuget/runtime) | `eng/Version.Details.xml`, `eng/Version.Details.props` | Any file outside `eng/` |\n| Codeflow (dotnet/dotnet) | `eng/Version.Details.xml` | Any `src/` files modified |\n| OneLoc | `**/xlf/*.xlf`, `**/Resources/*.resx` | Any non-localization file, or reviewers flagging reverted translations |\n\n### Step 3: Update Branches & Bump Versions (dependency PRs only)\n\nFor each **dependency update** PR:\n\n#### 3a. Update the PR branch\n\nUse the GitHub GraphQL `updatePullRequestBranch` mutation — the exact equivalent of the \"Update branch\" button:\n\n```bash\ngh api graphql -f query='\n  mutation {\n    updatePullRequestBranch(input: {pullRequestId: \"<NODE_ID>\"}) {\n      pullRequest { number headRefOid }\n    }\n  }\n'\n```\n\n#### 3b. Bump VersionPrefix (vs < 18.10 branches only)\n\n**Skip for `vs18.10` and newer vs* branches** - they don't need to increment the patch version with every dependency update anymore. Only bump for `vs*` branches that are older than `vs18.10`.\n\n1. Read `<VersionPrefix>X.Y.Z</VersionPrefix>` from the **target branch's** `eng/Versions.props`\n2. Increment patch: `X.Y.Z` → `X.Y.(Z+1)`\n3. Checkout the PR branch, update the version, commit, push:\n   ```bash\n   git fetch origin <head-branch>\n   git checkout <head-branch>\n   # Edit eng/Versions.props: set <VersionPrefix> to X.Y.(Z+1)\n   git add eng/Versions.props\n   git commit -m \"Bump VersionPrefix to X.Y.(Z+1)\"\n   git push origin <head-branch>\n   ```\n\n**Note:** The `<VersionPrefix>` line may be inline with `<DotNetFinalVersionKind>` — preserve that formatting.\n\n### Step 4: Produce the Dashboard\n\nOutput a **single dashboard** with all PRs grouped by category. This is the most important output — it lets the user click through and approve/reject each PR.\n\nFormat:\n\n```\n## 🚦 Dependency Update PRs\n\n| PR | Branch | CI | Review | Files | Suspicious? | Link |\n|----|--------|-----|--------|-------|-------------|------|\n| #13281 | main ← roslyn | ✅ SUCCESS | ✅ APPROVED (1) | 2 | ✅ Clean | https://github.com/dotnet/msbuild/pull/13281 |\n| #13310 | vs17.10 ← arcade | ❌ FAILURE | 🔶 REVIEW_REQUIRED | 14 | ✅ Clean (eng/ only) | https://github.com/dotnet/msbuild/pull/13310 |\n\n## 🔄 Codeflow PRs\n\n| PR | Branch | CI | Review | Files | Suspicious? | Link |\n|----|--------|-----|--------|-------|-------------|------|\n| #13258 | vs18.3 ← dotnet/dotnet | ✅ SUCCESS | 🔶 REVIEW_REQUIRED | 1 | ✅ Clean | https://github.com/dotnet/msbuild/pull/13258 |\n\n## 🌐 OneLoc PRs\n\n| PR | Branch | CI | Review | Files | Suspicious? | Link |\n|----|--------|-----|--------|-------|-------------|------|\n| #13290 | main | ⚪ None | ⛔ CHANGES_REQUESTED | 65 | ⚠️ Reverts translations | https://github.com/dotnet/msbuild/pull/13290 |\n```\n\nStatus icons:\n- CI: ✅ SUCCESS, ❌ FAILURE/ERROR, ⏳ PENDING, ⚪ None/EXPECTED\n- Review: ✅ APPROVED, 🔶 REVIEW_REQUIRED, ⛔ CHANGES_REQUESTED\n- Suspicious: ✅ Clean, ⚠️ with description\n\nAfter the tables, add a summary:\n> **N PRs total. M ready to merge, K need reviews, J have CI failures, L blocked.**\n\n## Version File Reference\n\nThe version is defined in `eng/Versions.props`:\n\n```xml\n<PropertyGroup>\n  <VersionPrefix>18.3.3</VersionPrefix>\n</PropertyGroup>\n```\n\n| Branch Pattern | VersionPrefix Format | Bump Needed? | Pre-release Label |\n|---------------|---------------------|-------------|------------------|\n| `main`        | `Major.Minor.0`     | No          | `preview`        |\n| `vs*`         | `Major.Minor.Patch` | Yes (+1)    | `servicing` or `preview` |\n\n## Troubleshooting\n\n| Problem | Solution |\n|---------|----------|\n| `updatePullRequestBranch` fails | PR may have conflicts GitHub can't auto-resolve — fall back to local `git merge` |\n| Push rejected after version bump | Branch was updated by maestro; re-fetch and retry |\n| VersionPrefix has no patch component (e.g. `18.6.0` on main) | This is main — skip the bump |\n| OneLoc PR flagged as \"reverting translations\" | Do NOT merge — wait for updated translations |\n\n<!-- File: .github/skills/optimizing-msbuild-performance/SKILL.md (Tokens: ~1183 | Category: anthropic-skill) -->\n---\nname: optimizing-msbuild-performance\ndescription: 'Guides performance optimization for MSBuild engine code. Consult when working on hot paths in evaluation or execution, reducing allocations, choosing collection types, handling strings efficiently, modifying Expander.cs or Evaluator.cs, using Span-based APIs and stackalloc, caching values, or profiling build performance. Also applies when reviewing PRs for performance regression.'\nargument-hint: 'Describe the performance-sensitive code area or optimization goal.'\n---\n\n# MSBuild Performance Guidelines\n\nMSBuild evaluates and builds thousands of projects in enterprise solutions. Performance is an architectural concern, not an afterthought.\n\n## Guiding Principle\n\n**Profile before optimizing; measure, do not guess.** Use BenchmarkDotNet, the evaluation profiler (`/profileevaluation`), or ETW traces to identify actual bottlenecks before optimizing.\n\nSee [evaluation-profiling.md](../../../documentation/evaluation-profiling.md) and [General_perf_onepager.md](../../../documentation/specs/proposed/General_perf_onepager.md) for profiling tools.\n\n## Allocation Awareness on Hot Paths\n\nThe evaluation and execution engines process millions of operations per build. Unnecessary allocations cause GC pressure that compounds across large solutions.\n\n### Rules\n\n1. **Avoid LINQ in hot paths.** `Where`, `Select`, `Any`, `First` all allocate enumerator objects and delegate closures. Use `foreach` loops instead.\n\n   ```csharp\n   // BAD — allocates iterator + delegate\n   var match = items.FirstOrDefault(i => i.Name == name);\n\n   // GOOD — zero allocations\n   foreach (var item in items)\n   {\n       if (item.Name == name) { /* found */ break; }\n   }\n   ```\n\n2. **Avoid string allocations in formatting.** Use `string.Concat`, interpolation with `Span<T>`, or `StringBuilder` reuse — not `string.Format` on hot paths.\n\n3. **Prefer `Span<T>` and `stackalloc` for short-lived buffers** when parsing or slicing strings. Avoid `Substring` when you only need to compare or inspect a portion.\n\n4. **Cache computed values.** If a value is computed in a loop but doesn't change, hoist it out. If it's expensive and reused across calls, use a field or `Lazy<T>`.\n\n## String Comparison Rules\n\nMSBuild property, item, and target names are **case-insensitive**. Getting this wrong causes subtle bugs and perf issues.\n\n| Scenario | Use |\n|----------|-----|\n| Property/item/target names | `MSBuildNameIgnoreCaseComparer` |\n| General MSBuild identifiers | `StringComparison.OrdinalIgnoreCase` |\n| Dictionary keys for MSBuild names | `MSBuildNameIgnoreCaseComparer` as comparer |\n| File paths on Windows | `StringComparison.OrdinalIgnoreCase` |\n| File paths on Linux | `StringComparison.Ordinal` |\n\n**Never** use `ToLower()`/`ToUpper()` for comparisons — they allocate a new string every time.\n**Never** use `CurrentCulture` for MSBuild identifiers — build behavior must not vary by locale.\n\n## Collection Type Selection\n\n| Access Pattern | Recommended Type |\n|---------------|-----------------|\n| Small fixed set (< ~8 items) | Array or `ReadOnlySpan<T>` |\n| Build-once, read-many | `ImmutableArray<T>` (not `ImmutableList<T>`) |\n| Keyed lookup, many items | `Dictionary<TKey, TValue>` with appropriate comparer |\n| Keyed lookup, few items (< 5) | Linear scan over array (cache-friendly, avoids dict overhead) |\n| Concurrent reads, rare writes | `ImmutableDictionary` or snapshot pattern |\n| Ordered iteration needed | `List<T>` or array; avoid `HashSet<T>` if order matters |\n\n**`ImmutableList<T>` is almost never the right choice** — it has O(log n) access vs O(1) for `ImmutableArray<T>`.\n\n## Hot Path Identification\n\nThese areas are performance-critical and require extra scrutiny:\n\n- **`Expander.cs`** — property/item/metadata expansion during evaluation\n- **`Evaluator.cs`** — project evaluation orchestration\n- **`ItemSpec.cs` / `LazyItemEvaluator.cs`** — item evaluation and globbing\n- **`TaskExecutionHost.cs`** — task parameter marshaling\n- **File I/O paths** — `FileMatcher.cs`, glob operations, project loading\n\n### Inlining Considerations\n\nFor extremely hot methods, consider:\n- `[MethodImpl(MethodImplOptions.AggressiveInlining)]` for small methods called millions of times\n- Avoiding virtual dispatch in inner loops\n- Using `struct` enumerators to avoid heap allocation\n\n## Evaluation Performance Specifics\n\n- **Import chain depth** affects evaluation time linearly. Minimize unnecessary imports.\n- **Glob patterns** are evaluated per-project. Overly broad globs (`**/*`) are expensive.\n- **Condition evaluation** should use short-circuit logic — put cheap checks first.\n- **Property functions** (`$([System.IO.Path]::...)`) are interpreted and slower than built-in operations.\n\n## Anti-Patterns\n\n| Anti-Pattern | Why It's Bad | Fix |\n|-------------|-------------|-----|\n| `items.Count() > 0` | Enumerates entire collection | `items.Any()` or check `.Count` property |\n| `string.Format` in log messages at Low importance | Allocates even if message is filtered | Use structured logging or guard with verbosity check |\n| `new List<T>(enumerable).ToArray()` | Double allocation | `enumerable.ToArray()` directly |\n| `dict.ContainsKey(k) then dict[k]` | Double lookup | `dict.TryGetValue(k, out var v)` |\n| Regex in a loop without `RegexOptions.Compiled` | Reinterprets pattern each time | Compile or use static `Regex` field |\n\n<!-- File: .github/skills/pipelines-health-check/SKILL.md (Tokens: ~5025 | Category: anthropic-skill) -->\n---\nname: pipelines-health-check\ndescription: Check health of MSBuild CI pipelines, VS repo PR insertion statuses, and VMR codeflow PRs. Use when asked about pipeline health, build failures, infrastructure issues, CI status, insertion PR status, VMR codeflow status, or for periodic health monitoring.\n---\n\n# Pipelines & PR Health Check\n\nThis skill checks the health of MSBuild's CI pipelines, the status of insertion PRs in the VS repository, and the health of VMR codeflow PRs that flow MSBuild source into the dotnet/dotnet unified repository.\n\n## When to Use\n\n- User asks about MSBuild pipeline health, CI status, or build failures\n- User asks about VS insertion PR status or whether insertions are going through\n- User asks about VMR codeflow PRs, dotnet/dotnet PR status, or dotnet-unified-build pipeline\n- User asks to check if there are failing checks on PRs\n- User asks for a health check or status overview\n- Periodic monitoring requests\n\n## Prerequisites\n\n- `az` CLI must be installed and authenticated (`az login` with access to the DevDiv organization)\n- Azure DevOps extension for `az` must be installed: `az extension add --name azure-devops`\n- PowerShell 5.1+ or PowerShell Core\n\n### Optional: WorkIQ (for infrastructure issue investigation)\n\n[WorkIQ](https://www.npmjs.com/package/@microsoft/workiq) is an MCP server / CLI that can query Microsoft 365 data (people, emails, Teams, documents) to find **service ownership, contacts, and incident context** when pipeline failures are caused by infrastructure issues outside MSBuild's control.\n\n**Check availability:**\n```powershell\nworkiq version\n# Expected: 0.2.x or later\n```\n\n**If not installed**, set it up:\n```powershell\n# Install globally (use --registry if your .npmrc redirects @microsoft scope to GitHub Packages)\nnpm install -g @microsoft/workiq --registry https://registry.npmjs.org\n\n# Accept the EULA (required once)\nworkiq accept-eula\n```\n\nWorkIQ is not required for the core health check. If unavailable, the skill will still work — it will simply skip the ownership lookup and suggest manual investigation or offer to help install WorkIQ.\n\n## Reference Information\n\n### Pipelines\n\n| Pipeline | Org/Project | ID | Purpose |\n|----------|-------------|----|---------|\n| MSBuild | devdiv/DevDiv | 9434 | Main CI pipeline — builds and tests on every commit to main |\n| MSBuild-OptProf | devdiv/DevDiv | 17389 | Optimization/profiling pipeline — runs on schedule |\n| dotnet-unified-build | dnceng-public/public | — | VMR build pipeline — runs on codeflow PRs in dotnet/dotnet |\n\n### Key URLs\n\n- MSBuild pipeline: `https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434`\n- OptProf pipeline: `https://devdiv.visualstudio.com/DevDiv/_build?definitionId=17389`\n- VS PRs assigned to MSBuild: `https://dev.azure.com/devdiv/DevDiv/_git/VS/pullrequests?_a=active&assignedTo=66cc9d27-aef7-4399-ba2c-3dccb4489098`\n- VMR codeflow PRs: `https://github.com/dotnet/dotnet/pulls?q=is:pr+is:open+\"Source+code+updates+from+dotnet/msbuild\"`\n- dotnet-unified-build (public): `https://dev.azure.com/dnceng-public/public/_build`\n\n## Phase 1: Collect Data & Present Overview Table\n\n### Step 1: Run all data collection scripts\n\nRun these three scripts from the repository root **in parallel**. They output JSON to stdout. Each script may take 1–3 minutes depending on the number of PRs and pipeline runs to fetch, so use an `initial_wait` of **120 seconds** or higher.\n\n```powershell\n# Pipeline health (checks both MSBuild and MSBuild-OptProf)\n$pipelineJson = & .\\.github\\skills\\pipelines-health-check\\check-pipeline-health.ps1\n\n# VS PR status (checks active non-Experimental PRs and last merged PR)\n$prJson = & .\\.github\\skills\\pipelines-health-check\\check-vs-pr-status.ps1\n\n# VMR codeflow status (checks open codeflow PRs from msbuild and their dotnet-unified-build runs)\n$vmrJson = & .\\.github\\skills\\pipelines-health-check\\check-vmr-codeflow.ps1\n```\n\nAll scripts write progress messages to stderr (`Write-Host`) and the JSON result to stdout.\n\nThe scripts sanitize error messages (stripping control characters, truncating to 500 chars) so the JSON output can be parsed directly with `ConvertFrom-Json` without additional cleanup.\n\n### Step 2: Present the overview table IMMEDIATELY\n\nParse the JSON outputs and render status overview tables to the user **before** doing any deeper investigation. This gives the user instant visibility.\nPresent ALL tables - for both pipelines and for the VS insertion PRs. Do not omit any of those unless explicitly asked by user just for some specific overview.\n\n#### Pipeline Health Table\n\nFor each pipeline in the JSON output, render one row:\n\n| Pipeline | Last Success | Age | Recent Runs | Status |\n|----------|-------------|-----|-------------|--------|\n| {pipelineName} ({pipelineId}) | {lastSuccessfulRun.finishTime} | {lastSuccessfulRun.ageHours}h | emoji sequence | status emoji + label |\n\n**Recent Runs column:** Show an emoji for each run in `recentRuns` array (newest first):\n- `✅` for `succeeded`\n- `❌` for `failed`\n- `⏳` for `inProgress`\n- `⚪` for `canceled` or other\n\n**Status column** — derive from `healthSummary` and `lastSuccessfulRun.ageHours`:\n- `✅ HEALTHY` — healthSummary starts with \"HEALTHY\"\n- `⚠️ FLAKY` — healthSummary starts with \"FLAKY\"\n- `🔴 UNHEALTHY` — healthSummary starts with \"UNHEALTHY\"\n- Add `⚠️` if ageHours > 24, `🔴` if ageHours > 48 (even if some runs succeed, stale success is a concern)\n\n#### VS Insertion PRs Table (non-Experimental)\n\nFor each PR in the `prs` array:\n\n| PR | Title | Checks ✅ | Checks ⏳ | Checks ❌ | Status |\n|----|-------|-----------|-----------|-----------|--------|\n| [{id}](url) | {title} (truncated) | {checks.succeeded} | {checks.pending} | {checks.failed} | status |\n\n**Status column:**\n- `🔴 Failing` — if `actionNeeded` is true (has failed required checks)\n- `⏳ Running` — if `checks.pending > 0` and no failures\n- `✅ Green` — if all checks succeeded or notApplicable\n\n#### Last Merged Insertion Row\n\n| Last Merged PR | Date | Age | Status |\n|---------------|------|-----|--------|\n| [{lastMergedPr.id}](lastMergedPr.url) | {lastMergedPr.closedDate} | {ageDays} days | status |\n\n**Status:**\n- `✅ Recent` — ageHours ≤ 48 (≤ 2 business days)\n- `⚠️ Getting stale` — ageHours > 48 and ≤ 96\n- `🔴 Stale insertion` — ageHours > 96 (> 4 business days)\n\n**Note on weekends:** When computing business-day age, be aware that weekends inflate the hour count. If today is Monday and the last merge was Friday, that's ~72h but only 1 business day. Mention this nuance to the user if the age seems borderline.\n\n#### VMR Codeflow PRs Table\n\nFor each codeflow PR in the `codeflowPRs` array from `$vmrJson`:\n\n| Codeflow PR | Age | Pipeline Runs | Upstream PRs | Status |\n|-------------|-----|---------------|--------------|--------|\n| [#{prNumber}](prUrl) | {prAge}h | emoji sequence from pipelineRuns | count | status emoji + label |\n\n**Pipeline Runs column:** Show an emoji for each run in `pipelineRuns` (newest first):\n- `✅` for `result == \"succeeded\"`\n- `❌` for `result == \"failed\"`\n- `⏳` for `status == \"inProgress\"`\n- `⚪` for other/no runs\n\n**Status column** — derive from `healthSummary`:\n- `✅ HEALTHY` — healthSummary starts with \"HEALTHY\"\n- `🔄 IN PROGRESS` — healthSummary starts with \"IN_PROGRESS\" or \"RETRYING\"\n- `🔴 FAILING` — healthSummary starts with \"UNHEALTHY\"\n- `⚠️ MIXED` — healthSummary starts with \"MIXED\"\n- `❓ UNKNOWN` — no pipeline runs found\n\n**If a PR has failures**, also render a failure details sub-table:\n\n| Failed Job | Failed Task | Error Category | Related Upstream PRs |\n|------------|------------|----------------|---------------------|\n| {job.name} | {task.name} | {category} | PR links from failureCorrelation |\n\nThe `failureCorrelation` array maps each failed build to error categories and potentially related upstream PRs (matched by title keywords). This helps quickly identify which msbuild change likely caused a VMR build failure.\n\n**Upstream PRs list:** For each codeflow PR, list the included upstream msbuild PRs:\n\n> **Included changes:** [#13175](url) Add App Host Support, [#13306](url) IBuildEngine callbacks, ...\n\n### Step 3: Identify problems\n\nAfter rendering the table, build a list of distinct problems. A \"problem\" is any of:\n\n1. **Pipeline failure** — A pipeline whose latest run on main failed, especially if `lastSuccessfulRun.ageHours > 24`\n2. **PR check failure** — An active non-Experimental PR that has `actionNeeded: true` (failed required checks)\n3. **Stale insertion** — `lastMergedPr.ageHours > 48` (no successful insertion in >2 business days)\n4. **All checks pending** — A PR where all checks are still pending/queued (may indicate a stuck pipeline or queue issue)\n5. **VMR codeflow failure** — A codeflow PR whose `healthSummary` starts with \"UNHEALTHY\" (dotnet-unified-build failing)\n6. **VMR codeflow stale** — A codeflow PR older than 48 hours with no successful pipeline run\n\nIf there are **no problems**, report `✅ ALL CLEAR — pipelines healthy, PRs on track, insertions flowing, VMR codeflow green` and stop. Do not proceed to Phase 2.\n\n## Phase 2: Investigate Problems via Subagents\n\nFor **each distinct problem** identified in Step 3, launch a **separate subagent** to perform DEEP, DETAILED investigation (use `#tool:agent/runSubagent` to run the investigation tasks). Fire them in parallel when possible. Use the <agent> template below to seed them. \n\n<subagent>\n\n### Subagent prompt templates\n\n#### For pipeline failures\n\n```\nInvestigate why the Azure DevOps pipeline \"{pipelineName}\" (ID: {pipelineId}) is failing.\n\nRecent failed runs on branch {branch}:\n{for each failed run, list: Run ID, start time, URL, and the failedTasks with their error messages}\n\nLast successful run: {lastSuccessfulRun.finishTime} ({ageHours} hours ago)\nURL: {lastSuccessfulRun.url}\n\nTasks:\n1. Categorize each failure as one of:\n   - BUILD ERROR: compilation failures, test failures, task execution errors in MSBuild code\n   - CONFIG/PERMISSION: signing errors, NuGet authentication, certificate issues, feed access\n   - INFRA/TRANSIENT: errors indicating unavailability or outage of services or resources\n2. Check if all recent failures share the same root cause or if there are different issues\n3. If infra/transient: suggest retrying the pipeline (provide the pipeline URL)\n4. If build error: \n  - Check the `For build errors` section below on how to investigate build errors with binlogs\n  - identify which component/task is failing and check recent commits to main to try to identify offending one.\n5. If infrastructure issues:\n  - Try to distill the exact reason for the issue, check if there are other failing pipelines with the same issue or any open bugs for the issue.\n  - **Use WorkIQ** to find the owning team and contacts. Check if `workiq` CLI is available (`workiq version`).\n    - If available, run: `workiq ask -q \"Who owns the {failing service/task name} service in Microsoft DevDiv? Who should be contacted about {brief error description}?\"`\n    - Include the WorkIQ response in your findings — it typically returns team names, distribution lists, contact people, and escalation paths.\n    - You can also ask WorkIQ about known outages: `workiq ask -q \"Are there any known outages or incidents for {service name} in Azure DevOps?\"`\n    - If WorkIQ is NOT available, note this in your report and suggest the user install it:\n      ```\n      npm install -g @microsoft/workiq --registry https://registry.npmjs.org\n      workiq accept-eula\n      ```\n  - Put together a concise overview of the issue, along with links to the failure messages, the owning team/contacts from WorkIQ, and suggested next steps.\n\nReturn: A comprehensive root cause analysis with category, explanation, links to failure messages, ownership info (from WorkIQ if available), and recommended action.\n```\n\n#### For PR check failures\n\n```\nInvestigate failing checks on VS insertion PR #{prId}: \"{prTitle}\"\nPR URL: {prUrl}\n\nFailed checks:\n{for each item in checks.failedChecks: genre, name, description, isRequired}\n\nPending checks (still running):\n{for each item in checks.pendingChecks: genre, name, description, isRequired}\n\nPipeline health context:\n{brief summary of pipeline health from Phase 1 — are pipelines also failing?}\n\nTasks:\n1. Identify which failed checks are required vs optional\n2. If required checks are failing, determine if this could be related to pipeline failures (same root cause)\n3. If checks are just pending/queued, note that they may still be running and suggest waiting\n4. Recommend specific actions: retry checks, investigate pipeline, or wait\n5. If check is failing - try to traverse the chain of called pipelines to the actual error, then: \n  - Check the `For build errors` section below on how to investigate build errors with binlogs\n  - identify which component/task is failing and check recent commits to msbuild main to try to identify offending one.\n\nReturn: Which checks need attention, likely cause, and recommended action.\n```\n\n#### For stale insertion\n\n```\nInvestigate why MSBuild insertions into VS appear stale.\n\nLast successfully merged non-Experimental PR: #{lastMergedPr.id} \"{lastMergedPr.title}\"\n  Merged: {lastMergedPr.closedDate} ({ageDays} days ago)\n  URL: {lastMergedPr.url}\n\nCurrently active non-Experimental PRs:\n{for each PR: id, title, url, checks summary, actionNeeded}\n\nPipeline health:\n{brief pipeline health summary}\n\nTasks:\n1. Check if there are active non-Experimental PRs waiting — if none, the issue may be that no insertion was triggered\n2. If there are active PRs with failing checks, identify if those failures are blocking the insertion\n3. If there are active PRs with all checks pending, they may just need time\n4. Correlate with pipeline health — if the CI pipeline is broken, insertions can't succeed\n5. Recommend specific actions to unblock\n\nReturn: Explanation of why insertion appears stuck and what to do about it.\n```\n\n#### For VMR codeflow failures\n\n```\nInvestigate failing dotnet-unified-build pipeline for VMR codeflow PR #{prNumber}: \"{prTitle}\"\nPR URL: {prUrl}\nPR Branch: {prBranch}\n\nThis is a codeflow PR that brings MSBuild source changes into the dotnet/dotnet VMR (Virtual Monolithic Repository).\nThe dotnet-unified-build pipeline runs in the dnceng-public/public Azure DevOps org.\n\nFailed pipeline runs:\n{for each failed run in pipelineRuns: buildId, buildNumber, URL, stages, failedJobs with their failedTasks and errors}\n\nFailure categories detected: {failureCorrelation[].categories}\n\nUpstream MSBuild PRs included in this codeflow:\n{for each PR in upstreamPRs: number, title, url, merged status}\n\nFailure-to-change correlation (from script):\n{for each item in failureCorrelation: buildId, categories, relatedUpstreamPRs}\n\nTasks:\n1. For each failed job, examine the error messages and categorize:\n   - TASK_HOST: MSB4216 errors about MSBuild not finding the task host executable — typically caused by changes to MSBuild's app host, node launching, or SDK layout\n   - SOURCE_BUILD_TASK_HOST: Same as TASK_HOST but in the source-only build (CentOS offline) — the previously-source-built SDK doesn't have the expected MSBuild executable\n   - COMPILATION: CS/VB/FS compilation errors — a code change broke the build\n   - BUILD_COMMAND: MSB3073 \"exited with code N\" — a build script or test failed\n   - NUGET_AUTH/SIGNING/TIMEOUT/RESOURCE: Infrastructure issues\n2. Use the failure-to-change correlation to identify which upstream MSBuild PR most likely caused each failure:\n   - Check if the error is in an area touched by one of the upstream PRs\n   - If TASK_HOST errors: look for PRs touching NodeLauncher, NodeProvider, task host, app host, or BuildEnvironmentHelper\n   - If COMPILATION errors: check which files the upstream PRs modified and whether any could cause the compilation break\n   - If test failures: identify which test is failing and which upstream PR likely affects that code path\n3. Check if there is an in-progress retry build that might resolve the issue\n4. If the failure appears to be an infrastructure issue (not caused by MSBuild changes), note that\n5. For build errors, check the `For build errors` section on how to investigate with binlogs\n6. Recommend specific actions:\n   - If an upstream PR is clearly at fault, suggest reverting it or filing a fix\n   - If a VMR-side fix is needed (e.g., SDK layout change), describe what needs to change\n   - If a retry might help (transient infra), suggest re-running the pipeline\n   - If the issue is already being retried (in-progress build), suggest waiting\n\nReturn: Root cause analysis mapping each failure to the likely upstream PR, with explanation, links, and recommended action.\n```\n\n#### For build errors\n\nTasks:\n1. Try to find a .binlog file(s) in the build or step artifacts and fetch it\n2. Ensure to acquire the [binlog-failure-analysis skill](https://github.com/ViktorHofer/dotnet-skills/blob/main/msbuild-skills/skills/binlog-failure-analysis/SKILL.md) together with the binlog-mcp (spawn via `dnx -y baronfel.binlog.mcp@0.0.13`)\n3. Use the binlog analysis skill and mcp to analyse the binlog(s) you found and analyse problems from those\n\n</subagent>\n\n## Phase 3: Final Report\n\nAfter all subagent results return, present the findings below the overview table under a **\"🔍 Problems & Recommendations\"** heading. For each problem:\n\n```markdown\n### Problem: {brief title}\n**Category:** {INFRA | BUILD | CONFIG | PR_CHECKS | STALE_INSERTION | VMR_CODEFLOW}\n**Details:** {subagent's explanation}\n**Ownership:** {owning team, contacts, DL from WorkIQ — include only for INFRA/CONFIG issues}\n**Recommended Action:** {subagent's recommendation}\n```\n\nIf all problems are infra/transient, add a note: *\"All current issues appear to be infrastructure-related. Consider retrying the pipelines and checking again in 30 minutes.\"*\n\n## Troubleshooting\n\n### \"az: command not found\" or \"az account get-access-token\" fails\nThe `az` CLI is not installed or not authenticated. Run `az login` first.\n\n### Scripts return empty arrays\n- Check that you have access to the DevDiv organization\n- The branch filter defaults to `main` — if checking a different branch, pass `-Branch <name>` to the pipeline script\n\n### PR statuses all show as \"pending\"\nThis is normal for newly created PRs. The checks take time to queue and run. If checks are pending for more than a few hours, this may indicate a stuck pipeline or queue issue.\n\n### Timeout or rate limiting\nIf the scripts take a long time or fail with 429 errors, Azure DevOps may be rate-limiting. Wait a minute and retry.\n\n### WorkIQ not found or EULA not accepted\nIf `workiq version` fails, install it:\n```powershell\nnpm install -g @microsoft/workiq --registry https://registry.npmjs.org\nworkiq accept-eula\n```\nNote: If your `.npmrc` redirects the `@microsoft` scope to GitHub Packages, use `--registry https://registry.npmjs.org` to override, or pass `--userconfig` pointing to a clean `.npmrc`.\n\n### WorkIQ returns empty or unhelpful results\nWorkIQ queries Microsoft 365 data (Outlook, Teams, SharePoint). Results depend on your account's access and the data available in your tenant. Try rephrasing the question or being more specific about the service name. Example queries that work well:\n- `workiq ask -q \"Who owns the MicroBuild service in Microsoft?\"`\n- `workiq ask -q \"Who owns the CloudBuild signing service in DevDiv?\"`\n- `workiq ask -q \"Who should I contact about NuGet feed authentication failures in Azure DevOps?\"`\n\n### VMR codeflow script returns no PRs\n- Verify there are actually open codeflow PRs: check `https://github.com/dotnet/dotnet/pulls?q=is:pr+is:open+\"Source+code+updates+from+dotnet/msbuild\"`\n- Verify `gh` CLI is authenticated: run `gh auth status`\n\n### VMR pipeline runs not found\n- The dotnet-unified-build pipeline runs in `dnceng-public/public`, which is publicly accessible\n- If no runs appear, the pipeline may not have been triggered yet for that PR\n- Codeflow PRs may take a few minutes after creation before CI triggers\n\n### JSON output too large or contains unexpected characters\n- Error messages from Azure DevOps timelines can contain Windows paths, control characters, and multi-KB stack traces\n- The scripts sanitize and truncate these to 500 characters — if you still see issues, check that you're running the latest version of the scripts\n- Use `ConvertFrom-Json` in PowerShell or `json.loads()` in Python to parse the output; avoid manual string manipulation\n\n<!-- File: .github/skills/project-management/SKILL.md (Tokens: ~3246 | Category: anthropic-skill) -->\n---\nname: project-management\ndescription: 'GitHub issue and project-board management for the dotnet/msbuild repo. Use when asked to file/triage/update issues, post comments, amend issue bodies, move sprints, bulk-update project board fields, or audit items by sprint/status/assignee.'\nargument-hint: 'Manage issues and project board entries for dotnet/msbuild.'\n---\n\n# Project Management for dotnet/msbuild\n\nThis skill covers everything you need to manage issues and the project board for the MSBuild repository: querying, commenting, editing, creating issues, and bulk operations on the project board (sprints, status, assignees, etc.).\n\n## Where things live\n\n- **Repository:** `dotnet/msbuild` — all issues, PRs, and discussions.\n- **Project board:** [`dotnet/projects/117`](https://github.com/orgs/dotnet/projects/117) — title \"MSBuild\". This is the canonical project board for the team. Most issues created in `dotnet/msbuild` are auto-added or manually added here for sprint planning.\n- **Sprints** are tracked via the **Sprint** iteration field on project 117. Iteration titles use the format `2026 May I`, `2026 May II`, `2026 June I`, etc. (roughly two-week iterations).\n\n> Note: there are/were other auxiliary project boards for specific initiatives. Use project 117 unless the user explicitly points at another one.\n\n### Stable identifiers for project 117\n\n| Identifier | Value |\n|---|---|\n| Project node ID | `PVT_kwDOAIt-yc4ABM5F` |\n| Sprint field ID (iteration) | `PVTIF_lADOAIt-yc4ABM5FzgAtMGI` |\n\nThese IDs are stable but can be re-derived if the project is ever recreated or migrated:\n```bash\n# Project node ID\ngh api graphql -f query='{ organization(login:\"dotnet\") { projectV2(number:117) { id } } }'\n\n# All field IDs (Sprint, Status, Area, Priority, …)\ngh project field-list 117 --owner dotnet --format json | jq '.fields[] | {id,name,type}'\n```\n\n## Tooling preference\n\nFor GitHub work, prefer `gh` CLI over MCP tools (per repo convention).\n\n- Use `gh issue ...` for issue-level operations (create / view / comment / edit / close).\n- Use `gh project ...` for project-board CRUD.\n- Use `gh api graphql` for anything `gh` doesn't expose directly (sprint values per item, project-level field values, bulk reads).\n\nThe default `gh` token may not include the `project` scope. If a `gh project ...` write call fails with a scope error, ask the user to run:\n```bash\ngh auth refresh -s project\n```\n\n## Common single-issue actions\n\n```bash\n# View\ngh issue view 13315 --repo dotnet/msbuild --json title,body,state,assignees,labels\n\n# Comment (use --body-file to avoid shell-quoting traps with multi-line markdown)\ngh issue comment 13315 --repo dotnet/msbuild --body-file ./comment.md\n\n# Amend body (overwrite — there is no append; use view → edit → write)\ngh issue edit 13597 --repo dotnet/msbuild --body-file ./new-body.md\n\n# Create\ngh issue create --repo dotnet/msbuild --title \"Title\" --body-file ./body.md\n\n# Add labels / assignees\ngh issue edit 13315 --repo dotnet/msbuild --add-label \"Area: Server\" --add-assignee someuser\n```\n\nWhen drafting comments / bodies that summarize internal discussions:\n- Use neutral, team-voice language (\"the team agreed\", \"open question\").\n- Scrub names, \"X said Y\", room/agent identifiers, and anything resembling internal politics.\n- Always write to a file and use `--body-file`. Don't try to inline multi-line markdown.\n\n## Querying issue → project field values (sprint, status, project-level assignees)\n\n`gh issue view` does **not** return project-board field values. Use GraphQL via the `repository → issue → projectItems` path:\n\n```bash\ngh api graphql -f query='\nquery {\n  r: repository(owner:\"dotnet\", name:\"msbuild\") {\n    issue(number: 13315) {\n      id\n      projectItems(first: 5) {\n        nodes {\n          id\n          project { number title }\n          sprint:  fieldValueByName(name: \"Sprint\")  { ... on ProjectV2ItemFieldIterationValue   { title iterationId } }\n          status:  fieldValueByName(name: \"Status\")  { ... on ProjectV2ItemFieldSingleSelectValue { name optionId } }\n        }\n      }\n    }\n  }\n}'\n```\n\n> `fieldValueByName` returns `null` when the field has no value set on that item — handle accordingly.\n\n## Bulk / board-wide queries\n\nThe `gh project item-list` CLI command has internal limits and is unreliable for large boards (project 117 is in the thousands of items). **Use GraphQL with pagination.**\n\n```python\nimport subprocess, json\n\nPROJECT_NUMBER = 117\nORG = \"dotnet\"\n\nresults = []\ncursor = None\nwhile True:\n    after = f', after: \"{cursor}\"' if cursor else ''\n    query = f'''query {{\n      organization(login: \"{ORG}\") {{\n        projectV2(number: {PROJECT_NUMBER}) {{\n          items(first: 100{after}) {{\n            pageInfo {{ hasNextPage endCursor }}\n            nodes {{\n              id\n              fieldValues(first: 20) {{\n                nodes {{\n                  ... on ProjectV2ItemFieldIterationValue {{\n                    field {{ ... on ProjectV2IterationField  {{ name }} }}\n                    title\n                  }}\n                  ... on ProjectV2ItemFieldUserValue {{\n                    field {{ ... on ProjectV2Field           {{ name }} }}\n                    users(first: 10) {{ nodes {{ login }} }}\n                  }}\n                  ... on ProjectV2ItemFieldSingleSelectValue {{\n                    field {{ ... on ProjectV2SingleSelectField {{ name }} }}\n                    name\n                  }}\n                }}\n              }}\n              content {{\n                ... on Issue        {{ number title }}\n                ... on PullRequest  {{ number title }}\n                ... on DraftIssue   {{ title }}\n              }}\n            }}\n          }}\n        }}\n      }}\n    }}'''\n    try:\n        r = subprocess.run(['gh','api','graphql','-f',f'query={query}'],\n                           capture_output=True, text=True, check=True)\n    except subprocess.CalledProcessError as e:\n        raise RuntimeError(f\"GraphQL query failed: {e.stderr}\") from e\n    page = json.loads(r.stdout)['data']['organization']['projectV2']['items']\n    for node in page['nodes']:\n        sprint = status = None\n        assignees = []\n        for fv in node.get('fieldValues', {}).get('nodes', []):\n            if not fv:                              # null entries for unset fields\n                continue\n            fname = (fv.get('field') or {}).get('name', '')\n            if fname == 'Sprint':\n                sprint = fv.get('title')\n            elif fname == 'Status':\n                status = fv.get('name')\n            elif fname == 'Assignees':\n                assignees = [u['login'] for u in (fv.get('users') or {}).get('nodes', [])]\n        results.append({'id': node['id'], 'sprint': sprint, 'status': status,\n                        'assignees': assignees, 'content': node.get('content')})\n    if not page['pageInfo']['hasNextPage']:\n        break\n    cursor = page['pageInfo']['endCursor']\n```\n\n## Updating project-board field values\n\n### Set a sprint (iteration) — first look up iteration IDs\n\nIteration IDs are short hex strings (e.g., `303c2425`). They are **not** the same across projects; always look them up from project 117.\n\n```bash\ngh api graphql -f query='\nquery {\n  node(id: \"PVTIF_lADOAIt-yc4ABM5FzgAtMGI\") {\n    ... on ProjectV2IterationField {\n      configuration {\n        iterations           { id title startDate }\n        completedIterations  { id title startDate }\n      }\n    }\n  }\n}' | jq '.data.node.configuration | (.iterations + .completedIterations)[] | {id,title}'\n```\n\n### Single-item field update — `gh project item-edit`\n\n```bash\n# Iteration / sprint\ngh project item-edit \\\n  --project-id PVT_kwDOAIt-yc4ABM5F \\\n  --id <PROJECT_ITEM_ID> \\\n  --field-id PVTIF_lADOAIt-yc4ABM5FzgAtMGI \\\n  --iteration-id <ITERATION_ID>\n\n# Single-select (Status, Area, Priority): use --single-select-option-id\n# Text: --text  | Number: --number  | Date: --date YYYY-MM-DD\n```\n\n### Single-item field update — GraphQL mutation (works the same)\n\n```bash\ngh api graphql -f query='\nmutation {\n  updateProjectV2ItemFieldValue(input: {\n    projectId: \"PVT_kwDOAIt-yc4ABM5F\",\n    itemId:    \"<PROJECT_ITEM_ID>\",\n    fieldId:   \"PVTIF_lADOAIt-yc4ABM5FzgAtMGI\",\n    value:     { iterationId: \"<ITERATION_ID>\" }\n  }) { projectV2Item { id } }\n}'\n```\n\n### Adding a freshly created issue to project 117\n\nA newly-created issue is not always auto-added. To add and set its sprint:\n\n```bash\nISSUE_NODE_ID=$(gh api graphql -f query='\n  query { repository(owner:\"dotnet\",name:\"msbuild\") { issue(number:13707) { id } } }' \\\n  | jq -r '.data.repository.issue.id')\n\nITEM_ID=$(gh api graphql -f query='\n  mutation { addProjectV2ItemById(input:{\n      projectId:\"PVT_kwDOAIt-yc4ABM5F\",\n      contentId:\"'\"$ISSUE_NODE_ID\"'\"\n  }) { item { id } } }' | jq -r '.data.addProjectV2ItemById.item.id')\n\ngh api graphql -f query='\n  mutation { updateProjectV2ItemFieldValue(input:{\n      projectId:\"PVT_kwDOAIt-yc4ABM5F\",\n      itemId:\"'\"$ITEM_ID\"'\",\n      fieldId:\"PVTIF_lADOAIt-yc4ABM5FzgAtMGI\",\n      value:{iterationId:\"<ITERATION_ID>\"}\n  }) { projectV2Item { id } } }'\n```\n\n## Common errors and gotchas\n\n### 1. `Field 'fieldValueByName' has an argument conflict`\n\n`gh api graphql --paginate` (and several jq-style aliases) re-uses field names. If you call `fieldValueByName(name:\"Sprint\")` and `fieldValueByName(name:\"Status\")` in the same selection set without aliases, you'll get:\n\n```\nField 'fieldValueByName' has an argument conflict: {name:\"Sprint\"} or {name:\"Status\"}?\n```\n\nFix: alias each call.\n\n```graphql\nsprint: fieldValueByName(name: \"Sprint\")  { ... }\nstatus: fieldValueByName(name: \"Status\")  { ... }\n```\n\n### 2. `gh api graphql --paginate` produces concatenated JSON, not one document\n\nEach page is emitted as a separate JSON object on stdout; piping straight into `jq` fails with \"Invalid numeric literal\". Either:\n- paginate manually in a loop (preferred for large reads), or\n- use `jq --slurp` to read the stream as an array.\n\n### 3. \"Selections can't be made directly on unions\"\n\n`field` on a project field value is a union; you can't select `name` directly. Use a typed inline fragment per concrete field type (`ProjectV2Field`, `ProjectV2IterationField`, `ProjectV2SingleSelectField`, …).\n\n### 4. Issue-level assignees ≠ project-board \"Assignees\" column\n\nThe board's \"No Assignees\" slice is driven by the **project-level Assignees field**, not by `Issue.assignees`. They can diverge. When filtering by board state, always read `ProjectV2ItemFieldUserValue` from `fieldValues`.\n\n### 5. `null` entries in `fieldValues.nodes`\n\nGraphQL returns `null` for items where a particular field is unset. Always guard `if not fv: continue` before dereferencing.\n\n### 6. DraftIssue items lack `number` / `repository`\n\n`content` may be a `DraftIssue` that only exposes `title`, `body`, `assignees`. Code that joins to issue numbers must handle `None`.\n\n### 7. `gh project item-list` is unreliable for large boards\n\nIt silently truncates. Always use the GraphQL pagination loop above instead.\n\n### 8. Missing `project` scope on `gh` token\n\nReads work; writes (`gh project item-edit`, the `addProjectV2ItemById` / `updateProjectV2ItemFieldValue` mutations) fail. Have the user run `gh auth refresh -s project`.\n\n### 9. `gh issue edit --body` overwrites — there is no append\n\nTo \"append\" to a body, fetch with `gh issue view --json body`, append, write back with `--body-file`. For status updates, prefer a comment.\n\n## Sanitization checklist for comments / body edits\n\nWhen summarizing meetings or chat threads into GitHub:\n\n- ✅ Use team voice (\"the team agreed\", \"open question\", \"follow-up needed\").\n- ✅ Keep objective, decision-relevant content (problem statement, scope, exit criteria, repro steps, numbers).\n- ❌ No personal names or \"X said / Y replied\".\n- ❌ No full email addresses of external contributors (use GitHub handles instead).\n- ❌ No internal-only identifiers (room names, recording IDs, agent fleet identifiers, partner-team contractor names).\n- ❌ No speculation about other teams' motives or anything resembling internal politics.\n- ❌ No confidential roadmap dates or unreleased product details unless already public.\n- Always show the user the proposed comment / body before posting (unless they've explicitly preapproved).\n\n## Common workflows\n\n| Goal | Sketch |\n|---|---|\n| Add a status comment to N issues | `for n in 13315 13702 …; do gh issue comment $n --repo dotnet/msbuild --body-file ./c-$n.md; done` |\n| Replace a one-line issue body with a full Context / Exit-criteria / Out-of-scope spec | `gh issue edit <n> --repo dotnet/msbuild --body-file ./body.md` |\n| Move all unassigned items from sprint X to sprint Y | GraphQL pagination loop above + `gh project item-edit --iteration-id` per item |\n| File a new issue and put it in the current sprint | `gh issue create` → `addProjectV2ItemById` → `updateProjectV2ItemFieldValue` |\n| Audit \"what's in the current sprint that has no owner\" | GraphQL pagination loop, filter on `Sprint == \"<current>\"` and empty `Assignees` |\n\n<!-- File: .github/skills/release/SKILL.md (Tokens: ~4156 | Category: anthropic-skill) -->\n---\nname: release\ndescription: 'Orchestrate an MSBuild release: create the tracking issue, branch, configure DARC channels and subscriptions, bump version in main, final-brand the release branch, insert into VS, and publish post-GA. Covers the full monthly release lifecycle aligned with VS shipping cadence.'\nargument-hint: 'Specify which phase to execute (0-5) and provide required inputs (THIS_RELEASE_VERSION, NEXT_VERSION, etc.)'\n---\n\n# MSBuild Release Orchestration\n\nThis skill guides an agent through the MSBuild release process defined in [`documentation/release-checklist.md`](../../../documentation/release-checklist.md). The checklist is the single source of truth — this skill provides context on how to execute it.\n\n## Overview\n\nMSBuild is a **component** that gets inserted into Visual Studio. VS ships monthly; MSBuild must branch and prepare its bits **before** VS is ready to take them. See the [release process doc](../../../documentation/release.md#how-msbuild-releases-flow-into-vs) for the full timeline diagram.\n\nThe insertion pipeline routes MSBuild branches to VS branches:\n- `main` → VS `main` (daily canary)\n- `vs*` release branch → VS `main` (replaces `main` → `main` after branch snap)\n\nVS handles the progression from `main` → `rel/insiders` → `rel/stable` on its own schedule. MSBuild's responsibility is to have final-branded bits in VS `main` before the insiders snap date.\n\nEach monthly VS release produces:\n- A new `vs*` branch from `main`\n- Final branding on that branch\n- A version bump in `main`\n- DARC channel/subscription updates\n- A VS insertion\n- Post-GA publishing to nuget.org and docs\n\nThe process is organized into **6 timeline-gated phases** (0–5), each with an explicit trigger.\n\n> **Execution model**: This skill is designed for an **interactive Copilot session**. The agent walks through each phase step-by-step, but every command that modifies state (git push, DARC writes, pipeline changes, PR creation) requires **user approval** before execution. Read-only queries (DARC get-*, git log, etc.) can run without approval.\n\n## Required Inputs\n\nBefore starting any phase, ensure you have these values (the user must provide them — version increments are irregular and cannot be computed):\n\n| Input | Example | How to determine |\n|---|---|---|\n| `PREVIOUS_RELEASE_VERSION` | `18.9` | Previous release branch version: the highest `vs*` version lower than `THIS_RELEASE_VERSION` (list candidates with `git branch -r --list 'origin/vs*'`). |\n| `PREVIOUS_RELEASE_EXACT_VERSION` | `18.9.6` | The version the previous release actually shipped as. From the previous release's tracking issue, or `git tag --list 'v18.9.*'`. Used by Phase 5.3a. |\n| `THIS_RELEASE_VERSION` | `18.10` | Current `VersionPrefix` in `eng/Versions.props` (drop `.0`) |\n| `NEXT_VERSION` | `18.11` | User-provided — not computable from current version |\n| `BRANCH_SNAP_DATE` | `YYYY-MM-DD` | From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) — when MSBuild branches `vs*` from main, insertion targets VS `main` |\n| `INSIDERS_SNAP_DATE` | `YYYY-MM-DD` | From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) — when VS snaps `main` → `rel/insiders`; final-branded bits must be in VS `main` before this |\n| `STABLE_SNAP_DATE` | `YYYY-MM-DD` | From [VS-Dates wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/49807/VS-Dates) — when VS promotes `rel/insiders` → `rel/stable` |\n| `VS_SHIP_DATE` | `YYYY-MM-DD` | When VS ships publicly (GA) — triggers post-release tasks |\n| `PACKAGE_VALIDATION_BASELINE_VERSION` | `18.10.0-1.26378.2` | See [How to determine `PACKAGE_VALIDATION_BASELINE_VERSION`](#how-to-determine-package_validation_baseline_version) below — non-trivial: most \"obvious\" picks are wrong. |\n\n> Version examples above track the current cycle (`eng/Versions.props` `VersionPrefix` is `18.10.0`). Dates are intentionally shown as a format only — always read the real ones from the VS-Dates wiki.\n\n### How to determine `PACKAGE_VALIDATION_BASELINE_VERSION`\n\n**The value is the latest `{{THIS_RELEASE_VERSION}}.0-<label>.<shortDate>.<rev>` MSBuild package that is both:**\n\n_`<label>` is `PreReleaseVersionLabel` from `eng/Versions.props` (currently `1`), so the versions look like `18.11.0-1.26426.2`. Do **not** assume the legacy `-preview-NNNNN-NN` shape — MSBuild no longer produces it._\n\n1. **Published on the public [dotnet-tools feed](https://dev.azure.com/dnceng/public/_artifacts/feed/dotnet-tools)** — this is the feed the official build publishes to and that ApiCompat restores baselines from. If the version isn't here, ApiCompat fails with `NU1102`.\n2. **Produced from a commit reachable from `vs{{THIS_RELEASE_VERSION}}`** — i.e. a commit on `vs{{THIS_RELEASE_VERSION}}`, or the `main` commit `vs{{THIS_RELEASE_VERSION}}` was branched from.\n\n**Two tempting wrong answers — and why they're wrong:**\n\n| Wrong pick | Why it fails |\n|---|---|\n| ❌ The release-versioned `{{THIS_RELEASE_VERSION}}.X` package that ships in VS / on nuget.org | Since [#14277](https://github.com/dotnet/msbuild/pull/14277) release branches build and insert **prerelease** versions, exactly like `main`; the release-versioned variants are produced by NuGetRepack at manual publish time. They never exist on the public CI feed, so ApiCompat cannot restore them. |\n| ❌ Blindly the most recent `{{THIS_RELEASE_VERSION}}.0-*` on `dotnet-tools` | After `vs{{THIS_RELEASE_VERSION}}` branches, `main` keeps producing `{{THIS_RELEASE_VERSION}}.0-*` until **this** main-bump PR merges — so the most recent feed entries may be `{{NEXT_VERSION}}`-content builds wearing `{{THIS_RELEASE_VERSION}}` branding. Picking one drifts the API baseline forward and silently hides real compat breaks. |\n\n**Timing caveat:** the branch-point build publishes to `dotnet-tools` asynchronously. If you branch and bump on the same day, the script may report the correct candidate as `[NOT on feed]` — wait for publication and re-run rather than substituting an older build.\n\n**Procedure:** run the helper — it does the whole resolution mechanically (requires `az login` with devdiv access):\n\n```\npwsh ./scripts/Get-PackageValidationBaseline.ps1 -ThisReleaseVersion {{THIS_RELEASE_VERSION}}\n# -> prints e.g. 18.11.0-1.26426.2\n```\n\nIt computes `git merge-base origin/main origin/vs{{THIS_RELEASE_VERSION}}`, finds the matching successful build in [pipeline 9434](https://devdiv.visualstudio.com/DevDiv/_build?definitionId=9434), derives the package version from the OfficialBuildId, and verifies it on the dotnet-tools feed. If it fails, read the script's own `.DESCRIPTION` header for the manual equivalent rather than reproducing it here.\n\n### Prerequisites\n- gh cli\n- az cli\n- darc cli — Arcade enforces a minimum version; if `darc` refuses with a \"below the minimum required version\" error, run `.\\eng\\common\\darc-init.ps1`\n\n## Phase Summary\n\n| Phase | Trigger | Key Actions |\n|---|---|---|\n| **0: Instantiate** | User-initiated | Validate inputs, create GitHub tracking issue |\n| **1: Branch & Prepare** | `BRANCH_SNAP_DATE` | Create `vs*` branch, DARC channel setup (batched PR), **audit all `vs*` branches for retirement** |\n| **2: DARC Subscription Updates** | Phase 1 branch exists (`vs*` created) | Retarget `main`-targeting subs + VMR backflow to next channel, retired-branch cleanup (batched PR), Arcade verify |\n| **3: Bump Main** | Phase 2 ingested and verified | Branding PR in `main` (`VersionPrefix` → next, ApiCompat baseline, refresh OptProf baseline) |\n| **4: Final Branding** | 7 days before `INSIDERS_SNAP_DATE` | Telemetry evidence checks, package API compatibility review, OptProf bootstrap, VS insertion before Insiders snap |\n| **5: Post-GA** | VS shipped (`VS_SHIP_DATE`) | Resolve the exact shipped version (SDK-coupled? SDK wins over VS `rel/stable`), nuget.org publish, docs, GitHub release, Change Waves Learn sync, retro |\n\n## DARC Batching\n\nDARC write commands push to the [maestro-configuration](https://dev.azure.com/dnceng/internal/_git/maestro-configuration) repo. Batch related changes into **one PR**:\n\n1. Choose a branch name like `release/msbuild-{{THIS_RELEASE_VERSION}}`\n2. Add `--configuration-branch <name> --no-pr` to every write command except the last\n3. Last command: use `--configuration-branch <name>` without `--no-pr` to create the PR\n4. Review the diff before merging; disable auto-complete if the PR must remain review-only.\n5. After merging, complete [DARC configuration PR verification](../../../documentation/release-checklist.md#darc-configuration-pr-verification) before dependent release steps.\n\nCheck existing channels and mappings before writing: the previous release normally pre-created the current release's mapping. Skip satisfied writes rather than constructing a redundant batch.\n\nRead-only commands (`get-default-channels`, `get-subscriptions`, `get-channel`) don't need these flags.\n\n**Non-interactive (`-q`).** `darc add-default-channel` / `add-subscription` prompt `y/n` when the target branch does not exist yet (e.g. pre-creating the `vs{{NEXT_VERSION}}` mapping in Phase 1.2c, or adding the new `vs{{THIS_RELEASE_VERSION}}` backflow in Phase 2). `darc delete-subscriptions` always prompts for confirmation. Console input is redirected in an agent session, so the prompt **fails or hangs the command** — always pass `-q` for these writes. Note the verb is `delete-subscriptions` (plural); `delete-subscription` does not exist.\n\n**Phase 2 — forward-flow rotation.** Move subscriptions targeting `main` (`dotnet/dotnet @ main`, `dotnet/fsharp @ main`) to the next channel so those consumers receive next-version builds. Keep VMR `release/*` targets, including `.NET-next` preview bands, on `VS {{THIS_RELEASE_VERSION}}` so the release branch continues to feed its SDK bands.\n\n**Phase 2 — VMR backflow rotation.** Backflow (`dotnet/dotnet → msbuild`, source-enabled) keeps each MSBuild branch's dependencies aligned with its SDK band. When the new `vs{{THIS_RELEASE_VERSION}}` is paired with an SDK band, repoint the `→ main` backflow to the **next** SDK band channel and add backflow from the **outgoing** band channel into the release branch. Skip this for a VS-only release. See checklist steps 2.2b / 2.3f / 2.3g.\n\n## Branch retirement (Phase 1.3)\n\nAudit every release branch to keep required SDK/VS servicing flows active and remove unused dependency automation.\n\n**Branch inventory:** [Maintained branches](../../../documentation/release.md#maintained-branches) is the static list. Update its branch rows, SDK/VS sources, and support deadlines when cutting or retiring branches; cross-check the live DARC mappings, subscriptions, and repository branches in checklist step 1.3a.\n\nCheck both lifecycles because either the SDK or Visual Studio can require a branch:\n\n| Lifecycle | Source |\n|---|---|\n| **SDK band** | [supported .NET versions table](https://learn.microsoft.com/dotnet/core/porting/versioning-sdk-msbuild-vs#supported-net-versions) — maps each feature band to its VS version and support end date |\n| **Visual Studio** | [VS Product Lifecycle and Servicing](https://learn.microsoft.com/visualstudio/releases/2026/servicing-vs) — annual releases and the LTSC table |\n\n**VS 2026 and later:** non-LTSC releases are serviced through Insiders and for one month after Stable release. A new Stable promotion ends VS support for the previous release. LTSC baselines receive one year of servicing after release; use the published LTSC table to identify them.\n\nUse published SDK and VS deadlines for lifecycle decisions, and DARC channels and subscriptions for dependency routing.\n\nApply the **combined rule**: retire a branch paired with an SDK band and a VS version only when both lifecycles agree it is out of support. Check these candidates for automation cleanup:\n\n| Signal | Meaning |\n|---|---|\n| Branch's `VS X.Y` channel has **no MSBuild outbound subscription** | No downstream consumes MSBuild builds from that branch. |\n| Default channel exists for a **branch that isn't in the repo** | Pure orphan — delete the mapping. |\n\n**Retirement cleanup (2.3e):** remove inbound subscriptions, unused MSBuild outbound subscriptions, and branch-to-channel associations, including disabled subscriptions. Preserve shared channels and flows still needed by other branches or producers; verify removals after merge and ingestion.\n\n## Executing a Phase\n\nWhen asked to execute a specific phase:\n\n1. Read the full phase from `documentation/release-checklist.md`\n2. Verify the trigger condition is met (previous phases completed, including ingestion and live verification of prerequisite DARC changes)\n3. Execute steps in order — respect sequential/parallel annotations\n4. For DARC commands: batch writes into one configuration PR per phase\n5. Record all output URLs in the tracking issue's artifact table\n6. Mark checkboxes as completed in the tracking issue\n7. In **Phase 4**, complete telemetry checkpoints 4.0 and 4.4a using the shared evidence procedure in `documentation/release-checklist.md`.\n8. In **Phase 4** (step 4.7): if `documentation/wiki/ChangeWaves.md` is changed for this release, update the public Learn page at https://learn.microsoft.com/visualstudio/msbuild/change-waves. Sync the Change Waves Learn page from `documentation/wiki/ChangeWaves.md` on the `vsXX.Y` branch that is live in VS Insiders / the latest preview SDK. PR goes to `MicrosoftDocs/visualstudio-docs-pr` (`docs/msbuild/change-waves.md`); example: https://github.com/MicrosoftDocs/visualstudio-docs-pr/pull/15662.\n\n## Key Files\n\n| File | Purpose |\n|---|---|\n| [`documentation/release-checklist.md`](../../../documentation/release-checklist.md) | **Operational checklist** — the source of truth |\n| [`documentation/release.md`](../../../documentation/release.md) | Maintained-branch inventory and release process |\n| [`documentation/wiki/ChangeWaves.md`](../../../documentation/wiki/ChangeWaves.md) | Source doc for the Learn page sync — always sync the `vsXX.Y` (Insiders/preview-SDK) copy, not `main` |\n| [MSBuild Change Waves Learn page](https://learn.microsoft.com/visualstudio/msbuild/change-waves) | Public docs target to [`MicrosoftDocs/visualstudio-docs-pr`](https://github.com/MicrosoftDocs/visualstudio-docs-pr) (`docs/msbuild/change-waves.md`) |\n| [`eng/Versions.props`](../../../eng/Versions.props) | `VersionPrefix`, `PackageValidationBaselineVersion`, `BootstrapSdkVersion` |\n| [`azure-pipelines/vs-insertion.yml`](../../../azure-pipelines/vs-insertion.yml) | VS insertion pipeline — `TargetBranch` parameter and `InsertTargetBranch` routing |\n| [`azure-pipelines/vs-insertion-experimental.yml`](../../../azure-pipelines/vs-insertion-experimental.yml) | Experimental insertion — `TargetBranch` parameter values |\n| [`scripts/Get-PackageValidationBaseline.ps1`](../../../scripts/Get-PackageValidationBaseline.ps1) | Phase 3.2 — resolves `PackageValidationBaselineVersion` deterministically (merge-base → pipeline 9434 → dotnet-tools feed) |\n| [`scripts/Get-LatestOptProfDrop.ps1`](../../../scripts/Get-LatestOptProfDrop.ps1) | Phase 3.3 — resolves the latest main OptProf drop (MSBuild-OptProf pipeline 17389) to refresh `OptProfBaselineDrop` in `.vsts-dotnet.yml` |\n| [`.vsts-dotnet.yml`](../../../.vsts-dotnet.yml) | Build pipeline entry point — `OptProfBaselineDrop` (hardcoded OptProf seed for new `vs*` branches) |\n| [`azure-pipelines/.vsts-dotnet-build-jobs.yml`](../../../azure-pipelines/.vsts-dotnet-build-jobs.yml) | Build jobs — `VisualStudio.ChannelName` is release-agnostic (`int.main`); no per-release edit |\n\n## Validation\n\nAfter completing all phases, verify:\n\n1. Branch `vs{{THIS_RELEASE_VERSION}}` exists and has final branding\n2. Main has `VersionPrefix` = `{{NEXT_VERSION}}.0`\n3. DARC: main → `VS {{NEXT_VERSION}}` channel, release branch → `VS {{THIS_RELEASE_VERSION}}` channel\n4. VS insertion PR merged\n5. Packages published to nuget.org\n6. GitHub release created with tag `v{{THIS_RELEASE_EXACT_VERSION}}`\n7. The Learn page https://learn.microsoft.com/visualstudio/msbuild/change-waves lists exactly the waves present in `vsXX.Y` (the version in VS Insiders / the latest released preview SDK), or the sync is explicitly tracked\n\n## Error Recovery\n\n- **Branch already exists**: Release was partially started — check the tracking issue for progress\n- **DARC channel already exists**: Safe to continue — `add-channel` is idempotent\n- **OptProf fails on first build**: Expected — that's why we use main's OptProf data as fallback\n- **DARC configuration PR conflicts**: Rebase the configuration branch on `production` and force-push\n\n## Major Version Releases\n\nIf `NEXT_VERSION` is a new major version (e.g., 18.x → 19.0), additional steps are needed after Phase 5. See [release.md](../../../documentation/release.md) for:\n- `src/Shared/BuildEnvironmentHelper.cs` — VS major version constants\n- `src/Shared/Constants.cs` — version constants\n- `src/Framework/Telemetry/TelemetryConstants.cs` — telemetry version\n\n<!-- File: .github/skills/reviewing-msbuild-code/SKILL.md (Tokens: ~166 | Category: anthropic-skill) -->\n---\nname: reviewing-msbuild-code\ndescription: \"Reviews MSBuild code changes using a 24-dimension methodology. Activates for code review, PR review, pull request analysis, design review, architecture review, code quality assessment, or style check of MSBuild code. Covers backwards compatibility, ChangeWave discipline, performance, allocation awareness, test coverage, error message quality, logging, string comparison, API surface, target authoring, cross-platform correctness, code simplification, concurrency, naming, SDK integration, evaluation model integrity, correctness, dependency management, security, and build infrastructure.\"\n---\n\n# MSBuild Code Review\n\nInvoke `@expert-reviewer` for 24-dimension MSBuild code review. The agent contains the full methodology, principles, dimension rules with checklists, folder hotspot mapping, and 4-wave workflow.\n\n<!-- File: .github/skills/running-unit-tests/SKILL.md (Tokens: ~2379 | Category: anthropic-skill) -->\n---\nname: running-unit-tests\ndescription: Guide for running MSBuild unit tests efficiently. Use when running, scoping, filtering, or speeding up unit tests in this repository, or when finalizing a change with a heavier validation pass. Covers xUnit v3 + Microsoft.Testing.Platform (MTP) specifics and which `dotnet test` flags do and don't apply.\nargument-hint: Run, filter, scope, or speed up MSBuild unit tests.\n---\n\n# Running MSBuild Unit Tests\n\nThis repo uses **xUnit v3** with the **Microsoft.Testing.Platform (MTP)** runner. Test projects are built as `OutputType=Exe`, so each test assembly is a self-contained host process — *not* a classic VSTest assembly.\n\nThere are **two ways to run tests**:\n\n| Method | When to use |\n|--------|-------------|\n| `dotnet test <project>` | Dev loop — run a single test project, optionally filtered |\n| `build.cmd -test` / `build.sh --test` | Final validation — builds everything and runs all test projects via the repo's Arcade harness |\n\nUse a **fast, scoped** `dotnet test` loop while iterating, and the **full `build.cmd -test`** pass before declaring the change done.\n\n## Repo-specific knobs to know about\n\nThese are configured in `Directory.Build.props` (repo root), `src/Directory.Build.targets`, and `src/Shared/UnitTests/xunit.runner.json`. They apply to both `dotnet test` and `build.cmd -test` unless noted otherwise:\n\n- **Multi-targeting**: test projects target .NETFramework and .NETCoreApp on Windows (.NETCoreApp only on Linux/macOS). `dotnet test` runs the suite **once per TFM**.\n- **Single-threaded by default**: `xunit.runner.json` sets `maxParallelThreads: 1` and `parallelizeTestCollections: false`. Many tests mutate process-global state (env vars, cwd, SDK resolvers), so this is intentional.\n- **Auto trait filters**: platform/TFM-inappropriate tests are filtered out via `--filter-not-trait Category=...` (e.g., `nonwindowstests`, `failing`, `nonnetcoreapptests`). Don't try to \"fix\" tests that appear skipped because of these.\n- **Coverage on non-Windows**: `--coverage --coverage-settings Coverage.config` is appended unconditionally to `XunitOptions` in `src/Directory.Build.targets`. There is no MSBuild property switch to disable it from `dotnet test` — to skip coverage, run the test exe directly without `--coverage`.\n- **Test runner**: `TestRunnerName=XUnitV3`, MTP `1.9.1`, xUnit v3 `3.2.2` (set in repo-root `Directory.Build.props`).\n- **DOTNET_HOST_PATH**: `RunnerUtilities.GetMSBuildEnvironmentVariables` (in `src/UnitTests.Shared/RunnerUtilities.cs`) sets `DOTNET_HOST_PATH` to the bootstrap dotnet when tests launch MSBuild as a child process, so tasks like `RoslynCodeTaskFactory` resolve the right host. Don't override `DOTNET_HOST_PATH` from a test.\n\n## Flags — MTP vs VSTest pitfalls\n\nThis repo uses MTP with the xUnit v3 MTP runner, **not** VSTest. Many familiar `dotnet test` flags silently do nothing. Key differences:\n\n- **Use** `--report-trx` (not `--logger trx`), `--coverage` (not `--collect \"XPlat Code Coverage\"`), `--filter-method`/`--filter-class`/`--filter-trait` for xUnit v3 native filtering.\n- **Don't use** `--filter`, `--nologo`, `--blame`, `--settings *.runsettings`, `--diag`, `--collect`, or `-- RunConfiguration.MaxCpuCount=...` — these are VSTest-only and ignored.\n- **Still works**: `-c`, `-f`, `--no-restore`, `--no-build`, `-v`, `-bl:`, `-p:Property=Value` — these are interpreted by `dotnet test` itself before MTP sees them.\n\nFor comprehensive MTP vs VSTest flag reference, see the `run-tests` skill from the `dotnet-test` plugin.\n\n## Dev loop: fast and scoped\n\nAim for sub-30s iterations.\n\n> Examples below use Windows-style backslashes and PowerShell line continuations. Forward slashes work everywhere with `dotnet` (`src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj`); on Linux/macOS use `/` and shell line continuations (`\\`), and the exe path becomes `artifacts/bin/<Proj>/Debug/net11.0/<Proj>` (no `.exe`).\n\n1. **Run via `dotnet test` with a single TFM and filter** (incremental build handles rebuilding automatically):\n   ```powershell\n   dotnet test src\\Tasks.UnitTests\\Microsoft.Build.Tasks.UnitTests.csproj `\n     -f net11.0 -- --filter-method \"*MyFeature*\"\n   ```\n2. **Or run the test exe directly** (fastest — no SDK overhead; build first if source changed):\n   ```powershell\n   dotnet build src\\Tasks.UnitTests\\Microsoft.Build.Tasks.UnitTests.csproj -c Debug -f net11.0\n   artifacts\\bin\\Microsoft.Build.Tasks.UnitTests\\Debug\\net11.0\\Microsoft.Build.Tasks.UnitTests.exe `\n     --filter-method \"*MyFeature*\" --no-progress\n   ```\n   The exe path is TFM-specific: `Debug\\net11.0\\...exe` for net11.0, `Debug\\net472\\...exe` for net472. Switching TFM without rebuilding silently runs stale binaries.\n\n### Speeding up the dev loop further\n\nThese trade safety for speed — use during iteration, **revert before final validation**:\n\n-- **Single TFM**: pass `-f net11.0`. Halves runtime on Windows by skipping `net472`.\n- **Temporarily relax single-threaded execution**: drop a `xunit.runner.json` next to the test project (or override the existing one) with:\n  ```json\n  {\n    \"$schema\": \"https://xunit.net/schema/current/xunit.runner.schema.json\",\n    \"longRunningTestSeconds\": 60,\n    \"maxParallelThreads\": -1,\n    \"parallelizeTestCollections\": true\n  }\n  ```\n  Or pass them as runner args after `--`: `-- xUnit.MaxParallelThreads=-1 xUnit.ParallelizeTestCollections=true`. Expect flakes in tests that touch env vars, cwd, the file system, or the global ProjectCollection — **don't ship a fix that depends on this being on**.\n- **Skip bootstrap packaging** for non-bootstrap test projects:\n  `dotnet build ... -p:CreateBootstrap=false` (useful when the local bootstrap SDK payload is missing).\n- **Narrow with traits**: `--filter-trait Category=mytraitduringdev` if you've tagged a focused subset.\n- **Skip code coverage on Linux/macOS**: run the test exe directly without `--coverage`. This repo does not expose a supported `dotnet test` property switch to disable the auto-added coverage arguments.\n\n## Final validation pass\n\nBefore saying \"done,\" run the heavy configuration. Don't skip TFMs and don't keep parallel-overrides.\n\n1. **Restore parallelism settings** (revert any local `xunit.runner.json` change).\n2. **Run all TFMs for affected projects**:\n   ```powershell\n   dotnet test src\\Build.UnitTests\\Microsoft.Build.Engine.UnitTests.csproj -c Release\n   dotnet test src\\Tasks.UnitTests\\Microsoft.Build.Tasks.UnitTests.csproj -c Release\n   # Add other UnitTests projects whose code paths you touched\n   ```\n3. **For broad changes (engine, framework, shared)**, run the full repo test suite (~9 minutes — do not cancel):\n   ```powershell\n   .\\build.cmd -test -c Release       # Windows\n   ./build.sh --test -c Release       # Linux/macOS\n   ```\n4. **Capture a TRX** if reporting results to the user or CI:\n   ```powershell\n   dotnet test <project> -c Release `\n     --report-trx --report-trx-filename validation.trx `\n     --results-directory artifacts\\TestResults\n   ```\n\n## Reading the summary line\n\nMTP's final summary reports `passed`, `failed`, and `skipped` separately. **Always check the `skipped` count** — platform-conditional attributes (`WindowsOnlyFact`, `UnixOnlyFact`, etc.) and the auto trait filters cause expected skips, but a sudden jump in skipped count can hide a regression where a test became inapplicable on the current platform without you intending it.\n\n## Picking the right project to run\n\nMatch the source area you changed to its `*.UnitTests` project:\n\n| Source area | Test project |\n|-------------|--------------|\n| `src/Build/**` (engine, evaluation, backend) | `src/Build.UnitTests/Microsoft.Build.Engine.UnitTests.csproj` |\n| `src/Framework/**` | `src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj` |\n| `src/Tasks/**` | `src/Tasks.UnitTests/Microsoft.Build.Tasks.UnitTests.csproj` |\n| `src/Utilities/**` | `src/Utilities.UnitTests/Microsoft.Build.Utilities.UnitTests.csproj` |\n| `src/MSBuild/**` (CLI) | `src/MSBuild.UnitTests/Microsoft.Build.CommandLine.UnitTests.csproj` |\n| `src/Build/BuildCheck/**` | `src/BuildCheck.UnitTests/Microsoft.Build.BuildCheck.UnitTests.csproj` |\n| `src/Shared/**` | Run the consumers above (Build, Tasks, Utilities) — shared code is linked into all of them. |\n\n## Quick reference\n\n| Scenario | Command |\n|----------|---------|\n| Fast scoped dev loop | `dotnet test <proj> -f net11.0 -- --filter-method \"*X*\"` |\n| Direct test exe | `artifacts\\bin\\<Proj>\\Debug\\net11.0\\<Proj>.exe --filter-method \"*X*\" --no-progress` |\n| Single test by name | `dotnet test <proj> -- --filter-method \"*MyTestMethod*\"` |\n| Final per-project validation | `dotnet test <proj> -c Release` (all TFMs) |\n| Final full validation | `.\\build.cmd -test -c Release` / `./build.sh --test -c Release` |\n| TRX report | `--report-trx --report-trx-filename out.trx --results-directory artifacts\\TestResults` |\n\n## See also\n\n- `.github/instructions/tests.instructions.md` — test authoring conventions (xUnit v3, Shouldly, `TestEnvironment`, `MockLogger`).\n- `src/Shared/UnitTests/xunit.runner.json` — repo-wide xUnit settings.\n- `src/Directory.Build.targets` — `XunitOptions`, auto trait filters, coverage wiring.\n- `documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md`\n\n<!-- File: .github/skills/use-bootstrap-msbuild/SKILL.md (Tokens: ~709 | Category: anthropic-skill) -->\n---\nname: use-bootstrap-msbuild\ndescription: Guide for testing bug reproductions against locally-built MSBuild. Use this when you have a repro project and want to verify a fix works before submitting a PR.\n---\n\n# Testing Bug Reproductions with Bootstrap MSBuild\n\nThis skill guides you through testing a bug reproduction project against your locally-built MSBuild to verify a fix.\n\n## Overview\n\nAfter making changes to MSBuild, you need to test them against a repro project. The \"bootstrap\" is a self-contained MSBuild installation built from your local changes. It includes all dependencies needed to build real projects.\n\n## Step 1: Build MSBuild with Bootstrap\n\nBuild MSBuild to create the bootstrap directory:\n\n```powershell\n# Windows\n.\\build.cmd\n\n# Unix/macOS\n./build.sh\n```\n\nThis creates the bootstrap at `artifacts\\bin\\bootstrap\\` with your changes.\n\nRerun this after making any code change. If run in the default mode, there may be some errors on subsequent builds about locked files (due to MSBuild worker node processes lingering). If so, run `./artifacts/bin/bootstrap/core/dotnet build-server shutdown`.\n\n## Step 2: Run Your Repro Project\n\n### .NET Core / .NET SDK Projects\n\nUse the bootstrap `dotnet` CLI directly (preferred):\n\n```powershell\n# Windows\nartifacts\\bin\\bootstrap\\core\\dotnet.exe build <path-to-repro.csproj>\n```\n\n```bash\n# Unix/macOS\n./artifacts/bin/bootstrap/core/dotnet build <path-to-repro.csproj>\n```\n\nAll of the usual command line arguments should work, including `-bl` to create binlogs.\n\n### .NET Framework Projects\n\nIf the problem is specific to the .NET Framework `MSBuild.exe` that is used in Visual Studio, and you're running on Windows, you can use the bootstrap MSBuild.exe directly:\n\n```powershell\nartifacts\\bin\\bootstrap\\net472\\MSBuild\\Current\\Bin\\MSBuild.exe <path-to-repro.csproj>\n```\n\n**Note**: The .NET Framework bootstrap output will only be complete when built on Windows using `MSBuild.exe`.\n\n### Changes not reflected\n\n1. Verify bootstrap was rebuilt: check `artifacts\\bin\\bootstrap\\core\\sdk\\*\\MSBuild.dll` timestamp\n2. Kill any lingering MSBuild server processes:\n   ```powershell\n   ./artifacts/bin/bootstrap/core/dotnet build-server shutdown\n   # Or use the helper function after sourcing msbuild-build-env.ps1\n   killdotnet\n   ```\n\n### Repro works with bootstrap but not with installed MSBuild\n\nYour fix is working! The repro uses your local changes while the installed MSBuild has the bug.\n\n## Quick Reference\n\n| Scenario | Command |\n|----------|---------|\n| Build bootstrap | `.\\build.cmd` |\n| .NET Core repro | `artifacts\\bin\\bootstrap\\core\\dotnet.exe build <project>` |\n| .NET Framework repro | `artifacts\\bin\\bootstrap\\net472\\MSBuild\\Current\\Bin\\MSBuild.exe <project>` |\n\n## See Also\n\n- [Bootstrap Documentation](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Bootstrap.md)\n- [Building and Debugging Guide](https://github.com/dotnet/msbuild/blob/main/documentation/wiki/Building-Testing-and-Debugging-on-Full-Framework-MSBuild.md)\n\n<!-- File: plugins/mt-migration/skills/multithreaded-task-migration/SKILL.md (Tokens: ~8452 | Category: anthropic-skill) -->\n---\nname: multithreaded-task-migration\ndescription: Guide for migrating MSBuild tasks to multithreaded mode support, including compatibility red-team review. Use this when converting tasks to thread-safe versions, implementing IMultiThreadableTask, adding TaskEnvironment support, or auditing migrations for behavioral compatibility.\n---\n\n# Migrating MSBuild Tasks to Multithreaded API\n\nMSBuild's multithreaded execution model requires tasks to avoid global process state (working directory, environment variables). Thread-safe tasks declare this capability via `MSBuildMultiThreadableTask` and use `TaskEnvironment` from `IMultiThreadableTask` for safe alternatives.\n\n## Migration Steps\n\n### Step 1: Update Task Class Declaration\n\na. Ensure the task implementing class is decorated with the `MSBuildMultiThreadableTask` attribute.\nb. Implement `IMultiThreadableTask` **only if** the task needs `TaskEnvironment` APIs (path absolutization, env vars, process start). If the task has no file/environment operations (e.g., a stub class), the attribute alone is sufficient.\n\n```csharp\n[MSBuildMultiThreadableTask]\npublic class MyTask : Task, IMultiThreadableTask\n{\n    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;\n    ...\n}\n```\n\n**Note**: `[MSBuildMultiThreadableTask]` has `Inherited = false` — it must be on each concrete class, not just the base. The corollary is easy to miss: the base class still executes multithreaded, but is not analyzed. Audit the whole base chain — see [Unsafe Code in an Unannotated Base Class](#unsafe-code-in-an-unannotated-base-class).\n\n### Step 2: Absolutize Paths Before File Operations\n\nAll path strings must be absolutized with `TaskEnvironment.GetAbsolutePath()` before use in file system APIs. This resolves paths relative to the project directory, not the process working directory.\n\n```csharp\nAbsolutePath absolutePath = TaskEnvironment.GetAbsolutePath(inputPath);\nif (File.Exists(absolutePath))\n{\n    string content = File.ReadAllText(absolutePath);\n}\n```\n\nThe [`AbsolutePath`](https://github.com/dotnet/msbuild/blob/main/src/Framework/PathHelpers/AbsolutePath.cs) struct:\n- `Value` — the absolute path string\n- `OriginalValue` — preserves the input path (use for error messages and `[Output]` properties)\n- Implicitly convertible to `string` for File/Directory API compatibility\n- `GetCanonicalForm()` — resolves `..` segments and normalizes separators (see [Sin 5](#sin-5-canonicalization-mismatch))\n\n**CAUTION**: `GetAbsolutePath()` throws `ArgumentException` for null/empty inputs. See [Sin 3](#sin-3-null-coalescing-that-changes-control-flow) and [Sin 6](#sin-6-exception-type-change) for compatibility implications.\n\n### Step 3: Replace Environment Variable APIs\n\n| BEFORE (UNSAFE)                                  | AFTER (SAFE)                                       |\n|--------------------------------------------------|----------------------------------------------------|\n| `Environment.GetEnvironmentVariable(\"VAR\");`       | `TaskEnvironment.GetEnvironmentVariable(\"VAR\");`     |\n| `Environment.SetEnvironmentVariable(\"VAR\", \"v\");`  | `TaskEnvironment.SetEnvironmentVariable(\"VAR\", \"v\");` |\n\n### Step 4: Replace Process Start APIs\n\n| BEFORE (UNSAFE - inherits process state)           | AFTER (SAFE - uses task's isolated environment)     |\n|----------------------------------------------------|-----------------------------------------------------|\n| `var psi = new ProcessStartInfo(\"tool.exe\");`      | `var psi = TaskEnvironment.GetProcessStartInfo();`  |\n|                                                    | `psi.FileName = GetFullPathToTool(); // must be absolute` |\n\n## Updating Unit Tests\n\nBuilt-in MSBuild tasks now initialize `TaskEnvironment` with a `MultiProcessTaskEnvironmentDriver`-backed default. Tests creating instances of built-in tasks no longer need manual `TaskEnvironment` setup. For custom or third-party tasks that implement `IMultiThreadableTask` without a default initializer, set `TaskEnvironment = TaskEnvironment.Fallback` (or use `TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(path)` to point at a specific project directory).\n\n## APIs to Avoid\n\n| Category | APIs | Alternative |\n|---|---|---|\n| **Forbidden** | `Environment.Exit`, `FailFast`, `Process.Kill`, `ThreadPool.SetMin/MaxThreads`, `Console.*` | Return false, throw, or use `Log` |\n| **Use TaskEnvironment** | `Environment.CurrentDirectory`, `Get/SetEnvironmentVariable`, `Path.GetFullPath`, `ProcessStartInfo` | See Steps 2-4 |\n| **Need absolute paths** | `File.*`, `Directory.*`, `FileInfo`, `DirectoryInfo`, `FileStream`, `StreamReader/Writer` | Absolutize first (File System APIs) |\n| **Need absolute paths (analyzer-invisible)** | `AssemblyName.GetAssemblyName`, `XDocument/XElement/XmlDocument.Load(string)`, `XmlReader/XmlWriter.Create(string)`, `ZipFile.*`, `X509CertificateLoader`, `Image.FromFile`, `Assembly.LoadFrom` | Absolutize first — **the analyzer does not flag these** |\n| **Review required** | `Assembly.Load*`, `Activator.CreateInstance*` | Check for version conflicts |\n\n### Analyzer-Invisible Path Consumers\n\n`MSBuildTask0003` monitors a fixed list of types (`File`, `Directory`, `FileInfo`, `DirectoryInfo`, `FileStream`, `StreamReader`, `StreamWriter`, `FileSystemWatcher`). Any *other* API that accepts a path string and touches disk is equally unsafe but produces **no diagnostic**.\n\nIn a ~150-task migration across dotnet/arcade, dotnet/source-build-assets and the dotnet/dotnet VMR, `AssemblyName.GetAssemblyName` on a raw input was the single most-repeated defect. Do not treat a clean analyzer run as evidence that path handling is complete — see [Verification](#verification-a-clean-analyzer-run-is-not-a-migration).\n\nNote the overload distinction: the *string* overloads are hazards; `XDocument.Load(stream)` and `new StreamReader(stream)` are fine, because the caller already resolved the path to open the stream.\n\n## Practical Notes\n\n### CRITICAL: Trace All Path String Usage\n\nTrace every path string through all method calls and assignments to find all places it flows into file system operations — including helper methods that may internally use File System APIs.\n\n1. Find every path string (e.g., `item.ItemSpec`, function parameters)\n2. Trace downstream through all method calls\n3. Absolutize BEFORE any code path that touches the file system\n4. Use `OriginalValue` for user-facing output (logs, errors) — see [Sin 2](#sin-2-errorlog-message-path-inflation)\n\n**Trace through abstractions, not just concrete calls.** A path can reach the file system without a single `System.IO` type appearing in the task. In `InstallDotNetTool` (dotnet/arcade), the `DestinationPath`, `DotnetPath` and `WorkingDirectory` inputs flow raw through `IFileSystem` and `ICommandFactory` interfaces — the task body looks completely clean, and the analyzer reports nothing. When a task input is passed to an interface or delegate, resolve it at the task boundary rather than hoping the implementation does.\n\n**Resolve once, at the boundary.** The recurring failure is not \"forgot to absolutize\" but \"absolutized at three of four use sites\". If a variable is going to be used as a path at all, convert it to `AbsolutePath` where it enters the task and keep it that way — see [Sin 8](#sin-8-swallowed-exceptions-hiding-an-unresolved-path).\n\n### Exception Handling in Batch Operations\n\nIn batch processing (iterating over files), `GetAbsolutePath()` throwing on one bad path aborts the entire batch. Match the original task's error semantics:\n\n```csharp\nbool success = true;\nforeach (ITaskItem item in SourceFiles)\n{\n    try\n    {\n        AbsolutePath path = TaskEnvironment.GetAbsolutePath(item.ItemSpec);\n        ProcessFile(path);\n    }\n    catch (ArgumentException ex)\n    {\n        Log.LogError(\"Invalid path '{0}': {1}\", item.ItemSpec, ex.Message);\n        success = false;\n    }\n}\nreturn success;\n```\n\n### Prefer AbsolutePath Over String\n\nStay in the `AbsolutePath` world — it's implicitly convertible to `string` where needed. Avoid round-tripping through `string` and back.\n\n### TaskEnvironment is Not Thread-Safe\n\nIf your task spawns multiple threads internally, synchronize access to `TaskEnvironment`. Each task *instance* gets its own environment, so no synchronization between tasks is needed.\n\n### API Discipline When Plumbing Through Helpers\n\nWhen the migration ripples into shared helpers:\n\n- **Helper signatures: take `AbsolutePath`, not `(string path, string pathForMessages)`**. Two-string signatures drift apart; the caller passing one `AbsolutePath` keeps `.Value` and `.OriginalValue` in lockstep.\n- **Repo-wide helpers belong in an extensions class** (e.g., `TaskEnvironmentExtensions`), not on the task. If a per-task helper looks generic, extract it.\n- **Don't condition behavior on MT-mode**. `TaskEnvironment.Fallback` already gives single-process semantics; `if (mtMode) { … } else { … }` doubles the maintenance surface and skips test coverage of one branch.\n- **Don't silently swallow `ArgumentException` from `GetAbsolutePath`** in a helper — log a diagnostic so customers can debug bad inputs.\n\n## References\n\n- [Thread-Safe Tasks Spec](https://github.com/dotnet/msbuild/blob/main/documentation/specs/multithreading/thread-safe-tasks.md)\n- [`AbsolutePath`](https://github.com/dotnet/msbuild/blob/main/src/Framework/PathHelpers/AbsolutePath.cs)\n- [`TaskEnvironment`](https://github.com/dotnet/msbuild/blob/main/src/Framework/TaskEnvironment.cs)\n- [`IMultiThreadableTask`](https://github.com/dotnet/msbuild/blob/main/src/Framework/IMultiThreadableTask.cs)\n\n---\n\n# Compatibility Red-Team Playbook\n\nAfter migration, review for behavioral compatibility. **Every observable difference is a bug until proven otherwise.**\n\nObservable behavior = `Execute()` return value, `[Output]` property values, error/warning message content, exception types, files written, and which code path runs.\n\n## The 8 Deadly Compatibility Sins\n\nReal bugs found during MSBuild task migrations. Every one shipped in initial \"passing\" code with green tests.\n\n**Edge-case discipline:** For every migrated code path, verify behavior when inputs are `null`, empty string (`\"\"`), or whitespace-only. `GetAbsolutePath` throws `ArgumentException` on null/empty — if the pre-migration code handled these differently (e.g., returned early, used a default, or threw a different exception type), the migration must preserve that behavior. Even if a scenario seems unlikely, treat it as a relevant finding if it is theoretically possible.\n\n### Sin 1: Output Property Contamination\n\nAbsolutized values leak into `[Output]` properties that users/other tasks consume.\n\n```csharp\n// BROKEN: ManifestPath was \"bin\\Release\\app.manifest\", now \"C:\\repo\\bin\\Release\\app.manifest\"\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(Path.Combine(OutputDirectory, name));\nManifestPath = abs; // implicit string conversion!\n\n// CORRECT: separate original form from absolutized path\nstring originalPath = Path.Combine(OutputDirectory, name);\nAbsolutePath outputPath = TaskEnvironment.GetAbsolutePath(originalPath);\nManifestPath = originalPath;          // [Output]: original form\ndocument.Save((string)outputPath);    // file I/O: absolute path\n```\n\n**Detect**: For every `[Output]` property, trace backward — is it ever assigned from an `AbsolutePath`?\n\n### Sin 2: Error/Log Message Path Inflation\n\nError messages and exception messages show absolutized paths instead of the user's original input.\n\n**Direct leakage** — passing an `AbsolutePath` to logging APIs:\n\n```csharp\n// BROKEN: \"Cannot find 'C:\\repo\\app.manifest'\" instead of \"Cannot find 'app.manifest'\"\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(path);\nLog.LogError(\"Cannot find '{0}'\", abs); // implicit conversion!\n\n// CORRECT: use OriginalValue\nLog.LogError(\"Cannot find '{0}'\", abs.OriginalValue);\n```\n\n**Indirect leakage** — exception messages from helpers that received the absolutized path:\n\n```csharp\n// BROKEN: ex.FileName / ex.Message embed the absolutized path\ncatch (FileNotFoundException ex) { Log.LogError(\"Not found: {0}\", ex.FileName); }\ncatch (Exception ex)              { Log.LogError(ex.Message); }\n\n// CORRECT: prefer the original input; if you must use the exception, sanitize\ncatch (FileNotFoundException ex) { Log.LogError(\"Not found: {0}\", abs.OriginalValue); }\ncatch (Exception ex)              { Log.LogError(ex.Message.Replace(abs.Value, abs.OriginalValue)); }\n```\n\n**Detect**: Search every `Log.LogError`/`LogWarning`/`LogMessage` — is any argument an `AbsolutePath`? Also check every `Log.LogError(ex.Message …)` / `ex.FileName` downstream of a `GetAbsolutePath` — did the exception originate from a helper that received the absolutized path?\n\n### Sin 3: Null Coalescing That Changes Control Flow\n\nAdding `?? \"\"` silently swallows an exception the old code relied on for error handling.\n\n```csharp\n// BEFORE: Path.GetDirectoryName(\"C:\\\") → null → Path.Combine(null, x) → ArgumentNullException\n//   → task fails with an exception / error logged → Execute() returns false\n\n// BROKEN: ?? \"\" added \"for safety\"\nstring dir = Path.GetDirectoryName(fileName) ?? string.Empty;\n// Path.Combine(\"\", x) succeeds silently → no error → Execute() returns TRUE!\n```\n\n**Detect**: For every `??` you added, ask: \"What happened when this was null before?\" If it threw and was caught → your `??` is a bug.\n\n### Sin 4: Try-Catch Scope Mismatch\n\n`GetAbsolutePath()` inside a try block leaves the absolutized value out of scope in the catch block. Helper methods in the catch (like `LockCheck`) then use the original non-absolute path.\n\n```csharp\n// CORRECT: hoist above try so catch can use it too\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(OutputManifest.ItemSpec);\ntry {\n    WriteFile(abs);\n} catch (Exception ex) {\n    string lockMsg = LockCheck.GetLockedFileMessage(abs);        // absolute → correct file\n    Log.LogError(\"Failed: {0}\", OutputManifest.ItemSpec, ...);   // original → user-friendly\n}\n```\n\n**Detect**: For every `GetAbsolutePath` inside a try, check if the catch block needs the absolutized value.\n\n### Sin 5: Canonicalization Mismatch\n\n`GetAbsolutePath` does NOT canonicalize. `Path.GetFullPath` does TWO things: absolutize AND canonicalize (`..` resolution, separator normalization). If the old code used `Path.GetFullPath` for dictionary keys, comparisons, or display, you must add `.GetCanonicalForm()`:\n\n```csharp\n// GetAbsolutePath(\"foo/../bar\")  → \"C:\\repo\\foo/../bar\"  (NOT canonical)\n// Path.GetFullPath(\"foo/../bar\") → \"C:\\repo\\bar\"         (canonical)\n\n// BROKEN for dictionary keys — \"C:\\repo\\foo\\..\\bar\" ≠ \"C:\\repo\\bar\"\nvar map = items.ToDictionary(p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec), ...);\n\n// CORRECT\nvar map = items.ToDictionary(\n    p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec).GetCanonicalForm(),\n    StringComparer.OrdinalIgnoreCase);\n```\n\n**Detect**: Find every `Dictionary`/`HashSet`/`ToDictionary` using path keys, and every place the old code called `Path.GetFullPath`. If canonicalization mattered, add `.GetCanonicalForm()`.\n\n### Sin 6: Exception Type Change\n\nOld code threw `FileNotFoundException` for missing files; new code throws `ArgumentException` from `GetAbsolutePath(\"\")` before reaching the file check. Custom catch blocks filtering by exception type may be bypassed. (`ExceptionHandling.IsIoRelatedException` catches `ArgumentException`, but task-specific handlers might not.)\n\n**Detect**: For every `GetAbsolutePath`, check what the old code threw for null/empty and whether the calling code has type-specific catch blocks.\n\n### Sin 7: `Path.IsPathRooted` as an Absolutize Gate\n\n`Path.IsPathRooted` returns `true` for **drive-relative** (`C:foo\\bar`) and **root-relative** (`\\foo\\bar`) Windows paths — both still depend on process current-directory / current-drive state. \"Absolutize only if not rooted\" silently leaves those paths process-dependent.\n\n`GetAbsolutePath` correctly handles all path forms — including the Windows edge cases (`C:foo`, `\\foo`) that `Path.IsPathRooted` considers \"rooted\" but that are still CWD/drive-dependent. Call it unconditionally; remove any `IsPathRooted` short-circuit.\n\n### Sin 8: Swallowed Exceptions Hiding an Unresolved Path\n\nThe usual mental model is \"unresolved path → `FileNotFoundException` → loud failure\". That model is wrong wherever a `catch` treats an exception as a *semantic answer*. There, an unresolved path does not fail the build — it silently returns the wrong answer.\n\nReal example from the dotnet/dotnet VMR (`CheckForPoison`), where the same variable is resolved three lines later:\n\n```csharp\n// BROKEN\ntry\n{\n    AssemblyName asm = AssemblyName.GetAssemblyName(fileToCheck);   // raw, relative-capable\n    ...\n    if (IsAssemblyFromSbrp(TaskEnvironment.GetAbsolutePath(fileToCheck))) { ... }   // resolved\n    else if (IsAssemblyPoisoned(TaskEnvironment.GetAbsolutePath(fileToCheck))) { ... }\n}\ncatch (BadImageFormatException) { /* not a managed assembly - fine */ }\n```\n\nThe `GetAssemblyName` call is only an \"is this a managed assembly?\" probe, and the catch means \"not an assembly\". So an unresolved path throws, gets swallowed, and **both poison checks are skipped** — turning a leaked binary into a false negative in the leak-detection gate itself. The build stays green while the check it exists to perform quietly stops working.\n\nThis sin is especially dangerous because it defeats the usual verification strategy: the decoy-CWD test (Pattern A) still \"passes\" unless it asserts on the *result*, not just on `Execute()` returning true.\n\n**Detect**: For every `catch` in a migrated task, ask what the handler *means*. If it means anything other than \"fail\" — \"not an assembly\", \"not found, use default\", \"unsupported, skip\" — then every path-consuming call inside the corresponding `try` must be absolutized, and the catch should be narrowed to the exception types that genuinely represent that semantic answer (`BadImageFormatException` here, **not** `IOException`/`ArgumentException`).\n\n---\n\n## Call-Chain Hazards Beyond the Task\n\nThe 7 sins above are *local* to the task body. The other half of MT migration is auditing the **transitive call chain** — helpers and shared utility classes the task reaches into.\n\n### Helpers That Capture Process State\n\nHelpers reached from `Execute()` can quietly depend on process state in any of these ways:\n\n- A relative path falls through to `Directory.GetCurrentDirectory()` (often as a \"fallback\") or to `Path.GetFullPath(x)` without a base.\n- An env var is read directly via `Environment.GetEnvironmentVariable` (not `TaskEnvironment`).\n- A `static` field is seeded from process state on first use (`static string s_x = Directory.GetCurrentDirectory()`), permanently capturing the first caller's environment for every later caller.\n- A BCL API echoes its input path back through `ex.Message` / `ex.FileName` (Sin 2).\n- The helper takes a `string path` and returns a `string` — losing the `.OriginalValue` distinction, so callers either lie in messages or absolutize twice.\n\n**Resolution patterns:**\n\n1. Add an MT-aware overload that takes `TaskEnvironment` (or an `AbsolutePath` / explicit fallback path). Legacy overload delegates with `TaskEnvironment.Fallback`. Don't branch on \"MT mode on/off\".\n2. Promote the parameter type to `AbsolutePath` so `.Value` and `.OriginalValue` travel together.\n3. Replace process-state-seeded static caches with `ConcurrentDictionary<TKey, TValue>` keyed on the inputs that determine uniqueness — never on process state.\n4. For cross-repo helpers (foundation types used by many tasks), migrate the foundation in a dedicated PR before the tasks that consume it; the hydration step is usually where `Path.GetFullPath` is hiding.\n\n### Unsafe Code in an Unannotated Base Class\n\n`[MSBuildMultiThreadableTask]` is `Inherited = false`, so it goes on each concrete task. The consequence people miss is the mirror image: **the base class runs multithreaded too, but nothing marks it as such** — and with `msbuild_task_analyzer.scope = multithreadable_only` (the recommended setting for incremental migration) the analyzer does not look inside it at all.\n\nFrom dotnet/arcade: `CreateAkaMSLinks` and `DeleteAkaMSLinks` were both annotated, both analyzer-clean. Their shared base contained:\n\n```csharp\n// AkaMSLinksBase - NOT annotated, so never analyzed\nFile.ReadAllText(ClientCertificate)   // ClientCertificate is a task input property\n```\n\nA monitored API (`File`) on a raw task input — exactly what the analyzer exists to catch — invisible purely because of where it lived.\n\n**Resolution:** when you annotate a task, audit its full base chain up to `Task`/`ToolTask`. If the base holds shared input properties or path handling, have the **base** implement `IMultiThreadableTask` and do the resolution there, so every derived task inherits the fix:\n\n```csharp\npublic abstract class AkaMSLinksBase : Task, IMultiThreadableTask\n{\n    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;\n    // ... resolve ClientCertificate here, once, for all derived tasks\n}\n```\n\nNote that implementing `IMultiThreadableTask` on the base is safe and does not change routing — routing keys off the attribute, which stays on the concrete classes.\n\n### Engine-Owned Shared State: `RegisterTaskObject`\n\nThe guidance about `static` fields has a less obvious sibling: `IBuildEngine4.GetRegisteredTaskObject` / `RegisterTaskObject`. Under MT, in-process thread nodes share this engine-owned state. Tasks in different worker or TaskHost processes do not.\n\nUsing these APIs is a **review signal, not a warning**. Check object thread safety, key collisions, and whether correctness depends on two task invocations sharing a cache.\n\nThe get/register sequence is not atomic. Two tasks can both miss and create an object. Only one equal-key object is retained; `RegisterTaskObject` returns no result, and MSBuild does not dispose an unretained object. Whether that matters depends on what is cached:\n\n| Cached computation | Review result |\n|---|---|\n| Pure and deterministic for the key | Safe if the losing object needs no cleanup, or the task cleans it up |\n| Deduplication is the point, such as logging once per build | Unsafe because both tasks can perform the side effect |\n| A failure | Check that the key includes every input that affects the result; otherwise do not cache it |\n\nReal examples from dotnet/arcade: `LocateDotNet` was safe because its computation was pure. `SingleError` was not safe because its purpose was to emit exactly one error per build, so its annotation was removed.\n\n### Tasks Instantiated Directly by Other Tasks\n\n`[MSBuildMultiThreadableTask]` is only honored when the TaskFactory creates the task (i.e., a target invokes it as `<MyTask … />`). A task created via `new MyTask()` inside another task's body bypasses the factory and **never gets `TaskEnvironment` injected** — it silently defaults to `TaskEnvironment.Fallback` (= process CWD).\n\nMigrating a nested task in isolation is therefore meaningless. Either migrate the parent and have it explicitly propagate its `TaskEnvironment` to the nested instance before calling `Execute()`, or restructure so the nested task is a regular TaskFactory-created task.\n\n**Detect:** grep for `new …Task()` followed by `.Execute()` — every direct instantiation is a hole.\n\n### ToolTask Override Hot Spots\n\n`ToolTask` subclasses inherit virtual methods that run *before or after* `Execute()` and frequently touch the file system. Audit every override:\n\n| Override | Hazard | Migration |\n|---|---|---|\n| `GenerateFullPathToTool()` | Builds `ProcessStartInfo.FileName` — relative tool path → wrong tool launched | Absolutize the tool path before returning |\n| `SkipTaskExecution()` | Up-to-date check on input/output timestamps using relative paths | Absolutize both sides of the comparison |\n| `ValidateParameters()` | `File.Exists` on input parameters | Absolutize before probing |\n| `GenerateResponseFileCommands()` / `GenerateCommandLineCommands()` | Tempting to absolutize args | **Don't** — the child's `WorkingDirectory` is the project dir, so relative args resolve correctly; absolutizing inflates user-visible output and can leak into tool diagnostics or generated artifacts |\n| `GetWorkingDirectory()` | Default `null` → child inherits host CWD | Leave alone if you use `TaskEnvironment.GetProcessStartInfo()`; it already sets `WorkingDirectory` |\n\n**Key contract:** `ProcessStartInfo.FileName` is resolved by the OS **before** `WorkingDirectory` takes effect, so the executable path must be absolute. Tool arguments are interpreted by the child process in its working directory, so they should stay relative.\n\n### Engine-Owned Environment Variables Are Immutable in MT Mode\n\nEnv vars consumed by the engine *before* tasks run (e.g., `MSBUILD*`-prefixed flags and the framework/SDK discovery variables) are snapshotted into engine caches at startup. Tasks that mutate them later have no observable effect on the engine — but the mutation appears to succeed locally, masking bugs.\n\nIf a migrated task previously mutated such a variable to influence a downstream tool, re-architect to pass the value via `ProcessStartInfo.EnvironmentVariables` on the child process instead of mutating the parent's environment.\n\n---\n\n## Test Patterns for MT Migrations\n\nA migration test must **fail when the migration is undone**. Tests that pass identically against pre- and post-migration code are theater. The two patterns below are the ones that reliably exercise MT-specific behavior.\n\n### Pattern A: Decoy-CWD Test (for tasks that touch the file system)\n\nSet the process working directory to a **decoy** dir with no relevant files; set `TaskEnvironment.ProjectDirectory` to a different dir with the inputs/expected outputs. The task must read/write against the project directory, not the decoy.\n\n```csharp\nusing TestEnvironment env = TestEnvironment.Create();\nTransientTestFolder projectDir = env.CreateFolder();\nTransientTestFolder decoyCwd = env.CreateFolder();\nFile.WriteAllText(Path.Combine(projectDir.Path, \"input.txt\"), \"expected\");\n\nenv.SetCurrentDirectory(decoyCwd.Path); // auto-restored when env is disposed\n\nvar task = new MyTask\n{\n    TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir.Path),\n    Input = \"input.txt\", // relative\n};\ntask.Execute().ShouldBeTrue();\n```\n\n**CRITICAL:** `Directory.SetCurrentDirectory` is **process-global**. xUnit parallelizes tests within a class by default — a CWD-mutating test will flake unless pinned to a non-parallel collection (`[Collection]` + `[CollectionDefinition(DisableParallelization = true)]`). The `IDisposable` restore protects sequential safety but not concurrent siblings.\n\n### Pattern B: Cross-Instance Independence (for tasks doing relative-path resolution)\n\nTwo task instances, two `TaskEnvironment.ProjectDirectory` values, same relative input. Assert each task's output is rooted to its own project directory — proving resolution is per-instance, not shared.\n\n### When NOT to Write a \"Migration Test\"\n\nAttribute-only migrations (just `[MSBuildMultiThreadableTask]`, no `IMultiThreadableTask`) on tasks whose `Execute()` body contains no file/env/process/static-state interactions have nothing to test — a decoy-CWD test would pass whether the attribute is present or not. **Don't write the test.** Document the call-chain audit conclusion in the PR description instead.\n\n### Test Hygiene\n\n- Use `TestEnvironment` / `TransientTestFolder` for temp dirs; never `Path.GetTempPath() + Guid.NewGuid()` with manual `try/finally`.\n- Hard-code expected assertion values; don't recompute them from `Path.Combine(...)` in the assertion — when the test fails you want to know what was actually wrong.\n\n---\n\n## Verification: A Clean Analyzer Run Is Not a Migration\n\n`Microsoft.Build.TaskAuthoring.Analyzer` is the right first line of defense, and it should be enabled. It is not sufficient on its own, and it is worth being concrete about why so you know what work it leaves you.\n\n**The data.** In a migration of ~150 tasks across dotnet/arcade, dotnet/source-build-assets and the dotnet/dotnet VMR, the analyzer was enabled in all three repos and every build was **0 warnings, 0 errors**. A manual audit afterwards found **9 real defects**:\n\n| Why the analyzer missed it | Count | Example |\n|---|---|---|\n| The code was outside the analysis scope | 2 | unsafe call in an unannotated base class |\n| The API is not on the monitored list | 3 | `AssemblyName.GetAssemblyName` |\n| The failure class is not modeled at all | 4 | task-object race, memoized failure, nested task construction, path crossing a DI boundary |\n\nThe last row is the important one: those are dataflow and lifetime problems, not banned-API problems. No amount of allowlist tuning reaches them, which is why the [Red-Team Audit Protocol](#red-team-audit-protocol) below is not optional.\n\n**Why \"it worked when I tested it\" is weak evidence.** An unresolved relative path in MT mode does not reliably throw. It resolves against whatever the shared node's current directory happens to be, which depends on which project scheduled first. So the same defect passes locally single-threaded every time, passes in CI most of the time, and fails occasionally on one machine under load. And per [Sin 8](#sin-8-swallowed-exceptions-hiding-an-unresolved-path), some never throw at all.\n\n**Verification checklist.** The [Sign-Off Checklist](#sign-off-checklist) at the end of this document is the actionable list. The items most likely to be skipped — because nothing warns you about them — are:\n\n1. The full base chain, not just the concrete class.\n2. Every `catch`, classified: does it mean \"fail\", or a semantic answer? (Sin 8)\n3. Every task input traced to *every* use site — the common defect is resolved at 3 of 4 sites, not 0 of 4.\n4. Caches and memoization: `static` fields, `RegisterTaskObject`, and anything storing a failure.\n5. A Pattern A or Pattern B test that fails when the migration is reverted — asserting on the *result*, not merely that `Execute()` returned true.\n\nIf your host supports agents, the `mt-migration-reviewer` agent automates most of this.\n\n## Migrating Is Not Always the Right Answer\n\nAdding the attribute is a claim that the task is safe to run concurrently in a shared process. When that claim cannot be made honestly, **leaving the task unannotated is a correct, supported outcome** — it simply keeps routing to the TaskHost sidecar, which is exactly the behavior it has today. A slower task is better than a wrong one.\n\nDe-annotate (or never annotate) when:\n\n- The task's semantics depend on being a singleton within the build — `SingleError` above is the canonical case.\n- The task deliberately mutates process-global state for a downstream consumer and cannot be re-architected to pass it via `ProcessStartInfo`.\n- The correctness argument depends on a race being benign and you cannot convince yourself it is.\n\nWhen you do this, leave a comment saying *why*, so the next person does not \"fix\" the missing attribute:\n\n```csharp\n// Deliberately not [MSBuildMultiThreadableTask]: this task must report exactly\n// one error per build, and the Get/RegisterTaskObject pair used to enforce that\n// is not atomic across concurrent instances in a shared node.\n```\n\n---\n\n## Red-Team Audit Protocol\n\n### Phase 1: Trace Every Changed Line\n\nFor each modified line: What was the exact runtime value before? After? Where does it flow (outputs, logs, file paths, dictionary keys)? Does each destination produce identical observable behavior?\n\n### Phase 2: Null/Empty/Edge Input Analysis\n\n| Input | `GetAbsolutePath` | Old behavior | Match? |\n|---|---|---|---|\n| `null` | `ArgumentException` | Varies | ❓ |\n| `\"\"` | `ArgumentException` | Varies | ❓ |\n| `\"C:\\\"` (root) | Valid | Valid | ✅ usually |\n| `\".\"` | `\"C:\\repo\\.\"` (not canonical) | `\"C:\\repo\"` if GetFullPath | ❌ maybe |\n| `\"foo\\..\\bar\"` | `\"C:\\repo\\foo\\..\\bar\"` | `\"C:\\repo\\bar\"` if GetFullPath | ❌ maybe |\n| Already absolute | Pass-through | Pass-through | ✅ |\n\n`Path.GetDirectoryName` → `Path.Combine` chains:\n| Input | `GetDirectoryName` returns | `Path.Combine(result, x)` |\n|---|---|---|\n| `\"C:\\\"` | `null` | Throws `ArgumentNullException` |\n| `\"\"` | `\"\"` (.NET Fx) / `null` (.NET Core+) | Works / Throws ⚠️ |\n| `\"file.resx\"` (no dir) | `\"\"` | Works |\n\nVerify behavior on **both** .NET Framework and .NET TFMs.\n\n### Phase 3: Downstream Impact\n\n1. **Output properties**: What consumes this task's `[Output]`? Does it compare, display, or use as a path?\n2. **Written files**: Does file content change? (e.g., XML with embedded paths)\n3. **Helper methods**: Do `LockCheck`, `ManifestWriter`, etc. internally resolve relative paths?\n4. **Error codes**: MSBuild error codes (MSBxxxx) must be identical.\n\n### Phase 4: Concurrency\n\n1. Two tasks with different `ProjectDirectory` values don't interfere\n2. No writes to static fields (shared across threads)\n3. Every registered task object is reviewed for thread safety, key collisions, registration races, and process-local visibility\n4. No cached *failures* — a failure computed under one task's environment must not be served to another\n5. All file operations use absolutized paths\n\n## Compatibility Test Matrix\n\n```\n[Task] × [Input Type] × [Assertion]\n\nInputs: relative path, absolute path, null, empty, \"..\" segments, root \"C:\\\",\n        forward slashes, trailing separator, UNC path, 260+ char path\n\nAssertions: Execute() return value, [Output] exact string, error message content,\n            exception type, file location, file content\n```\n\n## Sign-Off Checklist\n\n- [ ] `[MSBuildMultiThreadableTask]` on every concrete class (not just base — `Inherited=false`)\n- [ ] **Base chain audited** up to `Task`/`ToolTask` — unannotated bases still run multithreaded and are *not* analyzed\n- [ ] `IMultiThreadableTask` on classes that use `TaskEnvironment` APIs, with default initializer `= TaskEnvironment.Fallback`\n- [ ] Every `[Output]` property: exact string value matches pre-migration (Sin 1)\n- [ ] Every `Log.LogError`/`LogWarning`: path in message matches pre-migration (use `OriginalValue`) (Sin 2)\n- [ ] Every `Log.LogError(ex.Message …)` / `ex.FileName`: exception path sanitized to original input (Sin 2)\n- [ ] Every `GetAbsolutePath` call: null/empty exception behavior matches old code path (Sin 3, 6)\n- [ ] Every dictionary/set with path keys: canonicalization preserved (`GetCanonicalForm()`) (Sin 5)\n- [ ] Every try-catch: absolutized value available in catch block where needed (Sin 4)\n- [ ] Every `??` or `?.` added: verified it doesn't swallow a previously-thrown exception (Sin 3)\n- [ ] No `Path.IsPathRooted` short-circuits around `GetAbsolutePath` — call unconditionally (Sin 7)\n- [ ] Every `catch` classified — any handler meaning a *semantic answer* rather than \"fail\" has all path calls in its `try` absolutized, and is narrowed to the types representing that answer (Sin 8)\n- [ ] **Analyzer-invisible path consumers** absolutized: `AssemblyName.GetAssemblyName`, `XDocument`/`XmlDocument.Load(string)`, `XmlReader.Create(string)`, `ZipFile.*`, `X509CertificateLoader`, `Assembly.LoadFrom` — none of these produce a diagnostic\n- [ ] Every task input resolved at **every** use site, not most of them\n- [ ] Task inputs crossing an interface / delegate / DI boundary (`IFileSystem`, `ICommandFactory`) resolved at the task boundary\n- [ ] No `AbsolutePath` leaks into user-visible strings unintentionally\n- [ ] No behavior conditioned on \"MT mode on/off\" — `TaskEnvironment.Fallback` handles single-process case\n- [ ] **Call chain traced end-to-end** for every helper invoked from `Execute()`:\n    - No `Environment.CurrentDirectory` / `Directory.GetCurrentDirectory()` / `Path.GetFullPath(x)` without a base anywhere in the transitive call graph\n    - No direct `Environment.Get/SetEnvironmentVariable` (route through `TaskEnvironment`)\n    - No `static` mutable fields seeded from process state; replace with `ConcurrentDictionary` keyed on inputs\n    - Every `RegisterTaskObject` use reviewed for object thread safety, key collisions, registration races, and process-local visibility\n    - No cached *failures* whose key omits inputs that affect the result\n    - No `Console.*`, `Environment.Exit`, `Process.Kill`, `FailFast`\n- [ ] ToolTask overrides audited (`GenerateFullPathToTool`, `SkipTaskExecution`, `ValidateParameters`); `ProcessStartInfo.FileName` is absolute, tool *arguments* stay relative\n- [ ] No nested tasks created via `new …Task()` without explicit `TaskEnvironment` propagation\n- [ ] No mutations of engine-owned env vars (e.g. `MSBUILD*` and the framework/SDK discovery vars) in MT mode\n- [ ] Tests for custom tasks set `TaskEnvironment = TaskEnvironmentHelper.CreateForTest()` (built-in tasks have a default)\n- [ ] Migration test follows Pattern A (decoy-CWD) **or** Pattern B (cross-instance independence), or PR explains why no test is meaningful\n- [ ] CWD-mutating tests pinned to a non-parallel xUnit collection\n- [ ] Cross-framework: tested on .NET Framework and .NET TFMs.\n- [ ] If the task was **deliberately left unannotated**, a comment records why (see [Migrating Is Not Always the Right Answer](#migrating-is-not-always-the-right-answer))\n\n<!-- File: .github/plugin/marketplace.json (Tokens: ~135 | Category: marketplace) -->\n{\n  \"name\": \"msbuild-plugins\",\n  \"owner\": {\n    \"name\": \"dotnet/msbuild\"\n  },\n  \"metadata\": {\n    \"description\": \"GitHub Copilot plugins for the MSBuild ecosystem\",\n    \"version\": \"1.0.0\"\n  },\n  \"plugins\": [\n    {\n      \"name\": \"mt-migration\",\n      \"description\": \"MSBuild multithreaded task migration toolkit: the migration playbook + an MT-specific code reviewer that traces call chains end-to-end.\",\n      \"version\": \"0.1.0\",\n      \"source\": \"./plugins/mt-migration\"\n    }\n  ]\n}\n\n<!-- File: plugins/mt-migration/plugin.json (Tokens: ~124 | Category: plugin-manifest) -->\n{\n  \"name\": \"mt-migration\",\n  \"version\": \"0.1.0\",\n  \"description\": \"MSBuild multithreaded task migration toolkit: the migration playbook + an MT-specific code reviewer that traces call chains end-to-end. Portable across repos that consume Microsoft.Build.Framework's TaskEnvironment/IMultiThreadableTask API (e.g., dotnet/msbuild, dotnet/sdk, and any task assembly migrating to the MT model).\",\n  \"skills\": [\"./skills/\"],\n  \"agents\": [\n    \"./agents/mt-migration-reviewer.agent.md\"\n  ]\n}\n\n<!-- File: plugins/mt-migration/README.md (Tokens: ~765 | Category: plugin-manifest) -->\n# mt-migration plugin\n\nA GitHub Copilot plugin that bundles the MSBuild **multithreaded task migration** playbook plus an MT-specific PR reviewer.\n\nUse this on any repository that authors MSBuild tasks consuming the\n`IMultiThreadableTask` / `TaskEnvironment` / `[MSBuildMultiThreadableTask]` API\nsurface from `Microsoft.Build.Framework` — currently `dotnet/msbuild`,\n`dotnet/sdk`, and downstream task assemblies (NuGet, EF, ASP.NET Core, etc.).\n\n## Contents\n\n```\nmt-migration/\n├── plugin.json\n├── skills/\n│   └── multithreaded-task-migration/\n│       └── SKILL.md         # The 8 deadly sins, ToolTask hazards, helper patterns, test patterns, sign-off checklist\n└── agents/\n    └── mt-migration-reviewer.agent.md   # MT-specific PR reviewer that follows call chains end-to-end\n```\n\n## What it does\n\n| Component | When invoked | What it does |\n|---|---|---|\n| `multithreaded-task-migration` skill | Author migrating a task | Step-by-step migration recipe + compatibility red-team checklist. Distilled from ~50 merged migration PRs across `dotnet/msbuild` and `dotnet/sdk`. |\n| `mt-migration-reviewer` agent | Reviewing an MT migration PR | Delegates the 24-dimension general review to the host repo's expert reviewer (if any), then layers MT-specific findings on top. Mandatorily traces every call chain from `Execute()` to leaves and reports hazards by chain, not by file. Verifies tests are not theater. |\n\nThe reviewer is deliberately complementary to a generic code reviewer: it does\nnot re-explain migration steps, does not redo style/perf/naming review, and\nwill not flag a clean attribute-only migration as needing a concurrency test if\nthe call-chain audit comes back clean.\n\n## Installing\n\n### As a local plugin (from a clone of this repo)\n\n```sh\n# from a GitHub Copilot session\n/plugin install file://$(pwd)/plugins/mt-migration\n```\n\n### Across multiple repos (recommended)\n\nPublish this plugin folder to a small standalone repo (e.g.\n`your-org/mt-migration-plugin`) and install from there. The plugin contains no\nhard-coded references to `dotnet/msbuild` repo-local filesystem paths — only to\nthe public `Microsoft.Build.Framework` API surface (with source links for\nreference) — so it works on any task-authoring repo unchanged.\n\n```sh\n/plugin install github:your-org/mt-migration-plugin\n```\n\n## Relationship to the in-repo skill\n\nThis plugin is the **single, canonical** home of the MT migration skill. It is not duplicated under `.github/skills/` in `dotnet/msbuild` — install the plugin to use it locally. The skill-validator workflow in `dotnet/msbuild` only scans `.github/skills/` and `.github/agents/`, so the plugin lives outside that scope and is not subject to it.\n\n## Updating the skill from PR experience\n\nThe skill's \"deadly sins\" and call-chain hazard table grow as new\ndefect classes are found in merged migrations. To contribute:\n\n1. Find a defect that was caught in PR review and would have been missed by\n   the existing skill.\n2. Add a sin / hazard / pattern with a real-world citation (PR + line range).\n3. Update `skills/multithreaded-task-migration/SKILL.md`.\n4. Keep the agent file slim — it should keep delegating to the skill rather\n   than absorbing the rules itself.\n\n<!-- File: src/StringTools/README.md (Tokens: ~89 | Category: plugin-manifest) -->\n# Microsoft.NET.StringTools\n\nThis package contains the Microsoft.NET.StringTools assembly which implements common string-related functionality such as weak interning.\n\nAt this time, this is primarily an internal implementation detail of MSBuild and Visual Studio and we do not expect other consumers of the package. If you think you might like to use it, please start a discussion at https://github.com/dotnet/msbuild/discussions to let us know your use cases.\n</yakaai_skills>"}