diff --git a/.gitattributes b/.gitattributes
index 7c37579..4f89148 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,24 +1,8 @@
-# sln, csproj files (and friends) are always CRLF, even on linux
-*.sln text eol=crlf
-*.proj text eol=crlf
-*.csproj text eol=crlf
+# normalize by default
+* text=auto encoding=UTF-8
+*.sh text eol=lf
 
 # These are windows specific files which we may as well ensure are
 # always crlf on checkout
 *.bat text eol=crlf
 *.cmd text eol=crlf
-
-# Opt in known filetypes to always normalize line endings on checkin
-# and always use native endings on checkout
-*.c text
-*.config text
-*.h text
-*.cs text
-*.md text
-*.tt text
-*.txt text
-
-# Some must always be checked out as lf so enforce that for those files
-# If these are not lf then bash/cygwin on windows will not be able to
-# excute the files
-*.sh text eol=lf
\ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index ed06dbe..c95eb73 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -29,7 +29,7 @@ updates:
         - "Microsoft.AspNetCore*"
     Tests:
       patterns:
-        - "Microsoft.NET.Tests*"
+        - "Microsoft.NET.Test*"
         - "xunit*"
         - "coverlet*"
     ThisAssembly:
diff --git a/.github/release.yml b/.github/release.yml
index 9a018cd..c178589 100644
--- a/.github/release.yml
+++ b/.github/release.yml
@@ -8,7 +8,6 @@ changelog:
       - invalid
       - wontfix
       - need info
-      - docs
       - techdebt
     authors:
       - devlooped-bot
@@ -24,6 +23,7 @@ changelog:
     - title: πŸ“ Documentation updates
       labels: 
         - docs
+        - documentation
     - title: πŸ”¨ Other
       labels: 
         - '*'
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 47f63f6..a109549 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -4,21 +4,30 @@
 name: build
 on: 
   workflow_dispatch:
+    inputs:
+      configuration:
+        type: choice
+        description: Configuration
+        options: 
+        - Release
+        - Debug
   push:
     branches: [ main, dev, 'dev/*', 'feature/*', 'rel/*' ]
     paths-ignore:
       - changelog.md
-      - code-of-conduct.md
-      - security.md
-      - support.md
       - readme.md
   pull_request:
     types: [opened, synchronize, reopened]
 
 env:
   DOTNET_NOLOGO: true
+  PackOnBuild: true
+  GeneratePackageOnBuild: true
   VersionPrefix: 42.42.${{ github.run_number }}
   VersionLabel: ${{ github.ref }}
+  GH_TOKEN: ${{ secrets.GH_TOKEN }}
+  MSBUILDTERMINALLOGGER: auto
+  Configuration: ${{ github.event.inputs.configuration || 'Release' }}
 
 defaults:
   run:
@@ -31,7 +40,7 @@ jobs:
       matrix: ${{ steps.lookup.outputs.matrix }}
     steps:
       - name: 🀘 checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
         
       - name: πŸ”Ž lookup
         id: lookup
@@ -50,7 +59,7 @@ jobs:
         os: ${{ fromJSON(needs.os-matrix.outputs.matrix) }}
     steps:
       - name: 🀘 checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
         with: 
           submodules: recursive
           fetch-depth: 0
@@ -58,26 +67,10 @@ jobs:
       - name: πŸ™ build
         run: dotnet build -m:1 -bl:build.binlog
 
-      - name: βš™ GNU grep
-        if: matrix.os == 'macOS-latest'
-        run: |
-          brew install grep
-          echo 'export PATH="/usr/local/opt/grep/libexec/gnubin:$PATH"' >> .bash_profile
-
       - name: πŸ§ͺ test
-        uses: ./.github/workflows/test
-
-      - name: πŸ“¦ pack
-        run: dotnet pack -m:1 -bl:pack.binlog
-
-      # Only push CI package to sleet feed if building on ubuntu (fastest)
-      - name: πŸš€ sleet
-        env:
-          SLEET_CONNECTION: ${{ secrets.SLEET_CONNECTION }}
-        if: env.SLEET_CONNECTION != ''
         run: |
-          dotnet tool install -g --version 4.0.18 sleet 
-          sleet push bin --config none -f --verbose -p "SLEET_FEED_CONTAINER=nuget" -p "SLEET_FEED_CONNECTIONSTRING=${{ secrets.SLEET_CONNECTION }}" -p "SLEET_FEED_TYPE=azure" || echo "No packages found"
+          dotnet tool update -g dotnet-retest
+          dotnet retest -- --no-build
 
       - name: πŸ› logs
         uses: actions/upload-artifact@v3
@@ -86,11 +79,19 @@ jobs:
           name: logs
           path: '*.binlog'
 
+      - name: πŸš€ sleet
+        env:
+          SLEET_CONNECTION: ${{ secrets.SLEET_CONNECTION }}
+        if: env.SLEET_CONNECTION != ''
+        run: |
+          dotnet tool update sleet -g --allow-downgrade --version $(curl -s --compressed ${{ vars.SLEET_FEED_URL }} | jq '.["sleet:version"]' -r)        
+          sleet push bin --config none -f --verbose -p "SLEET_FEED_CONTAINER=nuget" -p "SLEET_FEED_CONNECTIONSTRING=${{ secrets.SLEET_CONNECTION }}" -p "SLEET_FEED_TYPE=azure" || echo "No packages found"
+
   dotnet-format:
     runs-on: ubuntu-latest
     steps:
       - name: 🀘 checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
         with: 
           submodules: recursive
           fetch-depth: 0
diff --git a/.github/workflows/changelog.config b/.github/workflows/changelog.config
index cd34ee7..e47bccd 100644
--- a/.github/workflows/changelog.config
+++ b/.github/workflows/changelog.config
@@ -1,7 +1,7 @@
 usernames-as-github-logins=true
 issues_wo_labels=true
 pr_wo_labels=true
-exclude-labels=bydesign,dependencies,duplicate,question,invalid,wontfix,need info,docs
+exclude-labels=bydesign,dependencies,duplicate,discussion,question,invalid,wontfix,need info,docs
 enhancement-label=:sparkles: Implemented enhancements:
 bugs-label=:bug: Fixed bugs:
 issues-label=:hammer: Other:
diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml
index b120b73..ca50e5a 100644
--- a/.github/workflows/changelog.yml
+++ b/.github/workflows/changelog.yml
@@ -17,7 +17,7 @@ jobs:
           github_token: ${{ secrets.GITHUB_TOKEN }}
           
       - name: 🀘 checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
           ref: main
diff --git a/.github/workflows/dotnet-file.yml b/.github/workflows/dotnet-file.yml
index 818aa2c..95f6228 100644
--- a/.github/workflows/dotnet-file.yml
+++ b/.github/workflows/dotnet-file.yml
@@ -24,7 +24,7 @@ jobs:
           github_token: ${{ secrets.GITHUB_TOKEN }}
 
       - name: 🀘 checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
         with:
           fetch-depth: 0
           ref: main
@@ -32,6 +32,7 @@ jobs:
 
       - name: βŒ› rate
         shell: pwsh
+        if: github.event_name != 'workflow_dispatch'
         run: |
           # add random sleep since we run on fixed schedule
           sleep (get-random -max 60)
@@ -70,7 +71,7 @@ jobs:
           validate: false
 
       - name: ✍ pull request
-        uses: peter-evans/create-pull-request@v4
+        uses: peter-evans/create-pull-request@v6
         with:
           base: main
           branch: dotnet-file-sync
diff --git a/.github/workflows/includes.yml b/.github/workflows/includes.yml
index bb1a90b..15a781e 100644
--- a/.github/workflows/includes.yml
+++ b/.github/workflows/includes.yml
@@ -21,7 +21,7 @@ jobs:
           github_token: ${{ secrets.GITHUB_TOKEN }}
 
       - name: 🀘 checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
         with: 
           token: ${{ env.GH_TOKEN }}
 
@@ -29,8 +29,9 @@ jobs:
         uses: devlooped/actions-includes@v1
 
       - name: ✍ pull request
-        uses: peter-evans/create-pull-request@v4
+        uses: peter-evans/create-pull-request@v6
         with:
+          add-paths: '**.md'
           base: main
           branch: markdown-includes
           delete-branch: true
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 34f5156..ae4240f 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -5,37 +5,53 @@
 name: publish
 on:
   release:
-    types: [released]
+    types: [prereleased, released]
 
 env:
   DOTNET_NOLOGO: true
   Configuration: Release
-
+  PackOnBuild: true
+  GeneratePackageOnBuild: true
+  VersionLabel: ${{ github.ref }}
+  GH_TOKEN: ${{ secrets.GH_TOKEN }}
+  MSBUILDTERMINALLOGGER: auto
+    
 jobs:
   publish:
-    runs-on: ubuntu-latest
+    runs-on: ${{ vars.PUBLISH_AGENT || 'ubuntu-latest' }}
     steps:
       - name: 🀘 checkout
-        uses: actions/checkout@v2
+        uses: actions/checkout@v4
         with: 
           submodules: recursive
           fetch-depth: 0
 
       - name: πŸ™ build
-        run: dotnet build -m:1 -p:version=${GITHUB_REF#refs/*/v} -bl:build.binlog
+        run: dotnet build -m:1 -bl:build.binlog
 
       - name: πŸ§ͺ test
-        uses: ./.github/workflows/test
-
-      - name: πŸ“¦ pack
-        run: dotnet pack -m:1 -p:version=${GITHUB_REF#refs/*/v} -bl:pack.binlog
-
-      - name: πŸš€ nuget
-        run: dotnet nuget push ./bin/**/*.nupkg -s https://api.nuget.org/v3/index.json -k ${{secrets.NUGET_API_KEY}} --skip-duplicate
+        run: |
+          dotnet tool update -g dotnet-retest
+          dotnet retest -- --no-build
 
       - name: πŸ› logs
         uses: actions/upload-artifact@v3
         if: runner.debug && always()
         with:
           name: logs
-          path: '*.binlog'
\ No newline at end of file
+          path: '*.binlog'
+
+      - name: πŸš€ nuget
+        env:
+          NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
+        if: ${{ env.NUGET_API_KEY != '' && github.event.action != 'prereleased' }}
+        working-directory: bin
+        run: dotnet nuget push *.nupkg -s https://api.nuget.org/v3/index.json -k ${{secrets.NUGET_API_KEY}} --skip-duplicate
+
+      - name: πŸš€ sleet
+        env:
+          SLEET_CONNECTION: ${{ secrets.SLEET_CONNECTION }}
+        if: env.SLEET_CONNECTION != ''
+        run: |
+          dotnet tool update sleet -g --allow-downgrade --version $(curl -s --compressed ${{ vars.SLEET_FEED_URL }} | jq '.["sleet:version"]' -r)        
+          sleet push bin --config none -f --verbose -p "SLEET_FEED_CONTAINER=nuget" -p "SLEET_FEED_CONNECTIONSTRING=${{ secrets.SLEET_CONNECTION }}" -p "SLEET_FEED_TYPE=azure" || echo "No packages found"
diff --git a/.github/workflows/sponsor.yml b/.github/workflows/sponsor.yml
deleted file mode 100644
index 9e47191..0000000
--- a/.github/workflows/sponsor.yml
+++ /dev/null
@@ -1,24 +0,0 @@
-name: sponsor πŸ’œ
-on: 
-  issues:
-    types: [opened, edited, reopened]
-  pull_request:
-    types: [opened, edited, synchronize, reopened]
-
-jobs:
-  sponsor:
-    runs-on: ubuntu-latest
-    continue-on-error: true
-    env:
-      token: ${{ secrets.GH_TOKEN }}
-    if: ${{ !endsWith(github.event.sender.login, '[bot]') && !endsWith(github.event.sender.login, 'bot') }}      
-    steps:
-      - name: 🀘 checkout
-        if: env.token != ''
-        uses: actions/checkout@v2
-          
-      - name: πŸ’œ sponsor 
-        if: env.token != ''
-        uses: devlooped/actions-sponsor@main
-        with:
-          token: ${{ env.token }}
diff --git a/.github/workflows/test/action.yml b/.github/workflows/test/action.yml
deleted file mode 100644
index 4a7dbae..0000000
--- a/.github/workflows/test/action.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-name: test
-description: runs dotnet tests with retry
-runs:
-  using: "composite"
-  steps:
-    - name: πŸ§ͺ test
-      shell: bash --noprofile --norc {0}
-      env:
-        LC_ALL: en_US.utf8
-      run: |
-        [ -f .bash_profile ] && source .bash_profile
-        counter=0
-        exitcode=0
-        reset="\e[0m"
-        warn="\e[0;33m"
-        while [ $counter -lt 6 ]
-        do
-            # run test and forward output also to a file in addition to stdout (tee command)
-            if [ $filter ]
-            then
-                echo -e "${warn}Retry $counter for $filter ${reset}"
-                dotnet test --no-build -m:1 --blame-hang --blame-hang-timeout 5m --filter=$filter | tee ./output.log
-            else
-                dotnet test --no-build -m:1 --blame-hang --blame-hang-timeout 5m | tee ./output.log
-            fi
-            # capture dotnet test exit status, different from tee
-            exitcode=${PIPESTATUS[0]}
-            if [ $exitcode == 0 ]
-            then
-                exit 0
-            fi
-            # cat output, get failed test names, remove trailing whitespace, sort+dedupe, join as FQN~TEST with |, remove trailing |.
-            filter=$(cat ./output.log | grep -o -P '(?<=\sFailed\s)[\w\._]*' | sed 's/ *$//g' | sort -u | awk 'BEGIN { ORS="|" } { print("FullyQualifiedName~" $0) }' | grep -o -P '.*(?=\|$)')
-            ((counter++))
-        done
-        exit $exitcode
diff --git a/.github/workflows/triage.yml b/.github/workflows/triage.yml
new file mode 100644
index 0000000..56ff299
--- /dev/null
+++ b/.github/workflows/triage.yml
@@ -0,0 +1,103 @@
+name: 'triage'
+on:
+  schedule:
+    - cron: '42 0 * * *'
+
+  workflow_dispatch:
+    # Manual triggering through the GitHub UI, API, or CLI
+    inputs:
+      daysBeforeClose:
+        description: "Days before closing stale or need info issues"
+        required: true
+        default: "30"
+      daysBeforeStale:
+        description: "Days before labeling stale"
+        required: true
+        default: "180"
+      daysSinceClose:
+        description: "Days since close to lock"
+        required: true
+        default: "30"
+      daysSinceUpdate:
+        description: "Days since update to lock"
+        required: true
+        default: "30"
+
+permissions:
+  actions: write # For managing the operation state cache
+  issues: write
+  contents: read
+
+jobs:
+  stale:
+    # Do not run on forks
+    if: github.repository_owner == 'devlooped' 
+    runs-on: ubuntu-latest
+    steps:
+      - name: βŒ› rate
+        shell: pwsh
+        if: github.event_name != 'workflow_dispatch'
+        env:
+          GH_TOKEN: ${{ secrets.DEVLOOPED_TOKEN }}        
+        run: |
+          # add random sleep since we run on fixed schedule
+          $wait = get-random -max 180
+          echo "Waiting random $wait seconds to start"          
+          sleep $wait
+          # get currently authenticated user rate limit info
+          $rate = gh api rate_limit | convertfrom-json | select -expandproperty rate
+          # if we don't have at least 100 requests left, wait until reset
+          if ($rate.remaining -lt 100) {
+              $wait = ($rate.reset - (Get-Date (Get-Date).ToUniversalTime() -UFormat %s))
+              echo "Rate limit remaining is $($rate.remaining), waiting for $($wait / 1000) seconds to reset"
+              sleep $wait
+              $rate = gh api rate_limit | convertfrom-json | select -expandproperty rate
+              echo "Rate limit has reset to $($rate.remaining) requests"
+          }
+        
+      - name: ✏️ stale labeler
+        # pending merge: https://github.com/actions/stale/pull/1176
+        uses: kzu/stale@c8450312ba97b204bf37545cb249742144d6ca69
+        with:
+          ascending: true # Process the oldest issues first
+          stale-issue-label: 'stale'
+          stale-issue-message: |
+            Due to lack of recent activity, this issue has been labeled as 'stale'. 
+            It will be closed if no further activity occurs within ${{ fromJson(inputs.daysBeforeClose || 30  ) }} more days. 
+            Any new comment will remove the label.
+          close-issue-message: |
+            This issue will now be closed since it has been labeled 'stale' without activity for ${{ fromJson(inputs.daysBeforeClose || 30  ) }} days.
+          days-before-stale: ${{ fromJson(inputs.daysBeforeStale || 180) }}  
+          days-before-close: ${{ fromJson(inputs.daysBeforeClose || 30  ) }}
+          days-before-pr-close: -1 # Do not close PRs labeled as 'stale'
+          exempt-all-milestones: true
+          exempt-all-assignees: true
+          exempt-issue-labels: priority,sponsor,backed
+          exempt-authors: kzu
+
+      - name: 🀘 checkout actions
+        uses: actions/checkout@v4
+        with:
+          repository: 'microsoft/vscode-github-triage-actions'
+          ref: v42
+
+      - name: βš™ install actions
+        run: npm install --production
+
+      - name: πŸ”’ issues locker
+        uses: ./locker
+        with:
+          token: ${{ secrets.DEVLOOPED_TOKEN }}
+          ignoredLabel: priority
+          daysSinceClose:  ${{ fromJson(inputs.daysSinceClose  || 30) }}
+          daysSinceUpdate: ${{ fromJson(inputs.daysSinceUpdate || 30) }}
+
+      - name: πŸ”’ need info closer
+        uses: ./needs-more-info-closer
+        with:
+          token: ${{ secrets.DEVLOOPED_TOKEN }}
+          label: 'need info'
+          closeDays:  ${{ fromJson(inputs.daysBeforeClose  || 30) }}
+          closeComment: "This issue has been closed automatically because it needs more information and has not had recent activity.\n\nHappy Coding!"
+          pingDays: 80
+          pingComment: "Hey @${assignee}, this issue might need further attention.\n\n@${author}, you can help us out by closing this issue if the problem no longer exists, or adding more information."
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 8c02bf3..2ac54a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,13 +1,14 @@
 bin
-app
 obj
 artifacts
 pack
 TestResults
 results
 BenchmarkDotNet.Artifacts
+/app
 .vs
 .vscode
+.genaiscript
 .idea
 local.settings.json
 
diff --git a/.netconfig b/.netconfig
index 3c4797f..6713968 100644
--- a/.netconfig
+++ b/.netconfig
@@ -22,25 +22,20 @@
 [file "SponsorLink.sln"]
 	url = https://github.com/devlooped/oss/blob/main/SponsorLink.sln
 	skip
-[file ".github/workflows/test/action.yml"]
-	url = https://github.com/devlooped/oss/blob/main/.github/workflows/test/action.yml
-	sha = 9a1b07589b9bde93bc12528e9325712a32dec418
-	etag = b54216ac431a83ce5477828d391f02046527e7f6fffd21da1d03324d352c3efb
-	weak
 [file ".github/workflows/changelog.yml"]
 	url = https://github.com/devlooped/oss/blob/main/.github/workflows/changelog.yml
-	sha = a4b66eb5f4dfb9704502f19f59ba33cb4855188c
-	etag = 54c0b571648b1055beb3ddac180b34e93a9869b9f0277de306901b2c1dbe0b2c
+	sha = 5fb172362c767bef7c36478f1a6bdc264723f8f9
+	etag = ad1efa56d6024ee1add2bcda81a7e4e38d0e9069473c6ff70374d5ce06af1f5a
 	weak
 [file ".github/dependabot.yml"]
 	url = https://github.com/devlooped/oss/blob/main/.github/dependabot.yml
-	sha = 35ca3f3405452465058d89005f8a88a65847c377
-	etag = f8080f8f04d87529e90d9a66751d304a7141196fb9734aa2d110784e52e66898
+	sha = 49661dbf0720cde93eb5569be7523b5912351560
+	etag = c147ea2f3431ca0338c315c4a45b56ee233c4d30f8d6ab698d0e1980a257fd6a
 	weak
 [file ".github/workflows/dotnet-file.yml"]
 	url = https://github.com/devlooped/oss/blob/main/.github/workflows/dotnet-file.yml
-	sha = f08c3f28e46e28eb31e70846d65e57aa9553ce56
-	etag = 567444486383d032c1c5fbc538f07e860f92b1d08c66ac6ffb1db64ca539251c
+	sha = 7afe350f7e80a230e922db026d4e1198ba15cae1
+	etag = 65e9794df6caff779eb989c8f71ddf4d4109b24a75af79e4f8d0fe6ba7bd9702
 	weak
 [file "license.txt"]
 	url = https://github.com/devlooped/oss/blob/main/license.txt
@@ -54,13 +49,13 @@
 	weak
 [file "src/Directory.Build.props"]
 	url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.props
-	sha = b1d14c6379e5820eb2c30e08bedbdf6e9c8e8cb2
-	etag = 33cd19e0f599f444c320406da3452e9e84d28c3bb13c09e9190d9d2e7f129545
+	sha = b76de49afb376aa48eb172963ed70663b59b31d3
+	etag = c8b56f3860cc7ccb8773b7bd6189f5c7a6e3a2c27e9104c1ee201fbdc5af9873
 	weak
 [file "src/Directory.Build.targets"]
 	url = https://github.com/devlooped/oss/blob/main/src/Directory.Build.targets
-	sha = 1bf1eacc7ac3920d52c8e7045bfa34abc7c05302
-	etag = 7cb1421f00d9f6f4c00f0ca98e485dcadb927cfa6b3f0b5d4fb212525d2ce9c0
+	sha = a8b208093599263b7f2d1fe3854634c588ea5199
+	etag = 19087699f05396205e6b050d999a43b175bd242f6e8fac86f6df936310178b03
 	weak
 [file "src/nuget.config"]
 	url = https://github.com/devlooped/oss/blob/main/src/nuget.config
@@ -72,23 +67,23 @@
 	weak
 [file "Gemfile"]
 	url = https://github.com/clarius/pages/blob/main/Gemfile
-	sha = 565a77f40db0863cb47ceb36f88790259a697c91
-	etag = 24e482e91192e292b633e3c17c4f095286ffb5a041d299d761b2e6ef99ee7669
+	sha = 90fa16ed0e7300a78a38ee1d23c34a7e875aab27
+	etag = 3dd7febc8ae6760f19abfe787711f469c288cd803a6f1c545edec34264d48e71
 	weak
 [file ".gitattributes"]
 	url = https://github.com/devlooped/oss/blob/main/.gitattributes
-	sha = 0683ee777d7d878d4bf013d7deea352685135a05
-	etag = 7acb32f5fa6d4ccd9c824605a7c2b8538497f0068c165567807d393dcf4d6bb7
+	sha = 5f92a68e302bae675b394ef343114139c075993e
+	etag = 338ba6d92c8d1774363396739c2be4257bfc58026f4b0fe92cb0ae4460e1eff7
 	weak
 [file ".github/workflows/build.yml"]
 	url = https://github.com/devlooped/oss/blob/main/.github/workflows/build.yml
 	weak
-	sha = 13d67e2cf3f786c8189364fd29332aaa7dc575dc
-	etag = c616df0877fba60002ccfc0397e9f731ddb22acbbb195a0598fedd4cac5f3135
+	sha = 5e17ad62ebb5241555a7a4d29e3ab15e5ba120d2
+	etag = f358acb1e45596bf0aad49996017da44939de30b805289c4ad205a7ccb6f99cb
 [file ".github/workflows/changelog.config"]
 	url = https://github.com/devlooped/oss/blob/main/.github/workflows/changelog.config
-	sha = 055a8b7c94b74ae139cce919d60b83976d2a9942
-	etag = ddb17acb5872e9e69a76f9dec0ca590f25382caa2ccf750df058dcabb674db2b
+	sha = 08d83cb510732f861416760d37702f9f55bd7f9e
+	etag = 556a28914eeeae78ca924b1105726cdaa211af365671831887aec81f5f4301b4
 	weak
 [file ".github/workflows/combine-prs.yml"]
 	url = https://github.com/devlooped/oss/blob/main/.github/workflows/combine-prs.yml
@@ -97,28 +92,23 @@
 	weak
 [file ".github/workflows/includes.yml"]
 	url = https://github.com/devlooped/oss/blob/main/.github/workflows/includes.yml
-	sha = ac753b791d03997eb655efb26ae141b51addd1c0
-	etag = fcd94a08ac9ebc0e8351deac4e7f085cf8ef67816cc50006e068f44166096eb8
+	sha = d152e7437fd0d6f6d9363d23cb3b78c07335ea49
+	etag = ec40db34f379d0c6d83b2ec15624f330318a172cc4f85b5417c63e86eaf601df
 	weak
 [file ".github/workflows/publish.yml"]
 	url = https://github.com/devlooped/oss/blob/main/.github/workflows/publish.yml
 	weak
-	sha = d3022567c9ef2bc9461511e53b8abe065afdf03b
-	etag = 58601b5a71c805647ab26e84053acdfb8d174eaa93330487af8a5503753c5707
-[file ".github/workflows/sponsor.yml"]
-	url = https://github.com/devlooped/oss/blob/main/.github/workflows/sponsor.yml
-	sha = 8990ebb36199046e0b8098bad9e46dcef739c56e
-	etag = e1dc114d2e8b57d50649989d32dbf0c9080ec77da3738a4cc79e9256d6ca5d3e
-	weak
+	sha = 5e17ad62ebb5241555a7a4d29e3ab15e5ba120d2
+	etag = 2cc96046d8f28e7cbcde89ed56d3d89e1a70fb0de7846ee1827bee66b7dfbcf1
 [file ".gitignore"]
 	url = https://github.com/devlooped/oss/blob/main/.gitignore
-	sha = ef852e7d2ec9a845dac272dfc479909c0bc6d9f3
-	etag = a556d6108892aa8e7e63476f4fad3a898b3ec1deda94332dd4e89d2fb6b555ca
+	sha = e0be248fff1d39133345283b8227372b36574b75
+	etag = c449ec6f76803e1891357ca2b8b4fcb5b2e5deeff8311622fd92ca9fbf1e6575
 	weak
 [file "Directory.Build.rsp"]
 	url = https://github.com/devlooped/oss/blob/main/Directory.Build.rsp
-	sha = ae25fae9d7daf0cb47d537ba870914aa3052f0c9
-	etag = 6a6c6e1d3895df953abf14c82b0899e3eea75cdcd679f6212dcfea15183d73d6
+	sha = 0f7f7f7e8a29de9b535676f75fe7c67e629a5e8c
+	etag = 0ccae83fc51f400bfd7058170bfec7aba11455e24a46a0d7e6a358da6486e255
 	weak
 [file "_config.yml"]
 	url = https://github.com/devlooped/oss/blob/main/_config.yml
@@ -132,6 +122,11 @@
 	weak
 [file ".github/release.yml"]
 	url = https://github.com/devlooped/oss/blob/main/.github/release.yml
-	sha = 1afd173fe8f81b510c597737b0d271218e81fa73
-	etag = 482dc2c892fc7ce0cb3a01eb5d9401bee50ddfb067d8cb85873555ce63cf5438
+	sha = 0c23e24704625cf75b2cb1fdc566cef7e20af313
+	etag = 310df162242c95ed19ed12e3c96a65f77e558b46dced676ad5255eb12caafe75
+	weak
+[file ".github/workflows/triage.yml"]
+	url = https://github.com/devlooped/oss/blob/main/.github/workflows/triage.yml
+	sha = 33000c0c4ab4eb4e0e142fa54515b811a189d55c
+	etag = 013a47739e348f06891f37c45164478cca149854e6cd5c5158e6f073f852b61a
 	weak
diff --git a/Directory.Build.rsp b/Directory.Build.rsp
index 7c0dbc1..509cc66 100644
--- a/Directory.Build.rsp
+++ b/Directory.Build.rsp
@@ -2,4 +2,4 @@
 -nr:false
 -m:1
 -v:m
--clp:Summary;ForceNoAlign
\ No newline at end of file
+-clp:Summary;ForceNoAlign
diff --git a/Gemfile b/Gemfile
index ed99566..fd95539 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,3 +1,3 @@
 source 'https://rubygems.org'
 
-gem 'github-pages', '~> 209', group: :jekyll_plugins
+gem 'github-pages', '~> 231', group: :jekyll_plugins
diff --git a/readme.md b/readme.md
index 297cdda..7094873 100644
--- a/readme.md
+++ b/readme.md
@@ -435,40 +435,34 @@ The versioning scheme for packages is:
 [![Clarius Org](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/clarius.png "Clarius Org")](https://github.com/clarius)
 [![Kirill Osenkov](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/KirillOsenkov.png "Kirill Osenkov")](https://github.com/KirillOsenkov)
 [![MFB Technologies, Inc.](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/MFB-Technologies-Inc.png "MFB Technologies, Inc.")](https://github.com/MFB-Technologies-Inc)
-[![Stephen Shaw](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/decriptor.png "Stephen Shaw")](https://github.com/decriptor)
 [![Torutek](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/torutek-gh.png "Torutek")](https://github.com/torutek-gh)
 [![DRIVE.NET, Inc.](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/drivenet.png "DRIVE.NET, Inc.")](https://github.com/drivenet)
-[![Daniel GnΓ€gi](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/dgnaegi.png "Daniel GnΓ€gi")](https://github.com/dgnaegi)
-[![Ashley Medway](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/AshleyMedway.png "Ashley Medway")](https://github.com/AshleyMedway)
 [![Keith Pickford](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/Keflon.png "Keith Pickford")](https://github.com/Keflon)
 [![Thomas Bolon](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/tbolon.png "Thomas Bolon")](https://github.com/tbolon)
 [![Kori Francis](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/kfrancis.png "Kori Francis")](https://github.com/kfrancis)
 [![Toni Wenzel](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/twenzel.png "Toni Wenzel")](https://github.com/twenzel)
-[![Giorgi Dalakishvili](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/Giorgi.png "Giorgi Dalakishvili")](https://github.com/Giorgi)
-[![Mike James](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/MikeCodesDotNET.png "Mike James")](https://github.com/MikeCodesDotNET)
+[![Uno Platform](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/unoplatform.png "Uno Platform")](https://github.com/unoplatform)
 [![Dan Siegel](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/dansiegel.png "Dan Siegel")](https://github.com/dansiegel)
 [![Reuben Swartz](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/rbnswartz.png "Reuben Swartz")](https://github.com/rbnswartz)
 [![Jacob Foshee](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/jfoshee.png "Jacob Foshee")](https://github.com/jfoshee)
 [![](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/Mrxx99.png "")](https://github.com/Mrxx99)
 [![Eric Johnson](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/eajhnsn1.png "Eric Johnson")](https://github.com/eajhnsn1)
-[![Norman Mackay](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/mackayn.png "Norman Mackay")](https://github.com/mackayn)
-[![Certify The Web](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/certifytheweb.png "Certify The Web")](https://github.com/certifytheweb)
 [![Ix Technologies B.V.](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/IxTechnologies.png "Ix Technologies B.V.")](https://github.com/IxTechnologies)
 [![David JENNI](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/davidjenni.png "David JENNI")](https://github.com/davidjenni)
 [![Jonathan ](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/Jonathan-Hickey.png "Jonathan ")](https://github.com/Jonathan-Hickey)
-[![Oleg Kyrylchuk](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/okyrylchuk.png "Oleg Kyrylchuk")](https://github.com/okyrylchuk)
 [![Charley Wu](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/akunzai.png "Charley Wu")](https://github.com/akunzai)
 [![Jakob TikjΓΈb Andersen](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/jakobt.png "Jakob TikjΓΈb Andersen")](https://github.com/jakobt)
-[![Seann Alexander](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/seanalexander.png "Seann Alexander")](https://github.com/seanalexander)
 [![Tino Hager](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/tinohager.png "Tino Hager")](https://github.com/tinohager)
 [![Mark Seemann](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/ploeh.png "Mark Seemann")](https://github.com/ploeh)
-[![Angelo Belchior](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/angelobelchior.png "Angelo Belchior")](https://github.com/angelobelchior)
 [![Ken Bonny](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/KenBonny.png "Ken Bonny")](https://github.com/KenBonny)
 [![Simon Cropp](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/SimonCropp.png "Simon Cropp")](https://github.com/SimonCropp)
 [![agileworks-eu](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/agileworks-eu.png "agileworks-eu")](https://github.com/agileworks-eu)
-[![](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/sorahex.png "")](https://github.com/sorahex)
+[![sorahex](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/sorahex.png "sorahex")](https://github.com/sorahex)
 [![Zheyu Shen](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/arsdragonfly.png "Zheyu Shen")](https://github.com/arsdragonfly)
 [![Vezel](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/vezel-dev.png "Vezel")](https://github.com/vezel-dev)
+[![ChilliCream](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/ChilliCream.png "ChilliCream")](https://github.com/ChilliCream)
+[![4OTC](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/4OTC.png "4OTC")](https://github.com/4OTC)
+[![Vincent Limo](https://raw.githubusercontent.com/devlooped/sponsors/main/.github/avatars/v-limo.png "Vincent Limo")](https://github.com/v-limo)
 
 
 <!-- sponsors.md -->
diff --git a/src/Directory.Build.props b/src/Directory.Build.props
index e2a7cc4..381c383 100644
--- a/src/Directory.Build.props
+++ b/src/Directory.Build.props
@@ -46,8 +46,6 @@
 
   <PropertyGroup Label="Build">
     <Configuration Condition="'$(Configuration)' == '' and $(CI)">Release</Configuration>
-    <GenerateDocumentationFile>true</GenerateDocumentationFile>
-    <GenerateDocumentationFile Condition="$(MSBuildProjectName.Contains('Tests'))">false</GenerateDocumentationFile>
     <LangVersion>Latest</LangVersion>
 
     <!-- See https://docs.microsoft.com/en-us/dotnet/standard/assembly/reference-assemblies -->
@@ -118,6 +116,8 @@
 
   <PropertyGroup Label="Version" Condition="$(VersionLabel) != ''">
     <_VersionLabel>$(VersionLabel.Replace('refs/heads/', ''))</_VersionLabel>
+    <_VersionLabel>$(_VersionLabel.Replace('refs/tags/v', ''))</_VersionLabel>
+
     <!-- For PRs, we just need a fixed package version numbered after the PR # itself, so remove the commits # at the end -->
     <_VersionLabel Condition="$(_VersionLabel.Contains('refs/pull/'))">$(VersionLabel.TrimEnd('.0123456789'))</_VersionLabel>
     <!-- Next replace the prefix for simply 'pr', so we end up with 'pr99/merge' by default -->
@@ -128,7 +128,9 @@
     <_VersionLabel>$(_VersionLabel.Replace('/', '-'))</_VersionLabel>
 
     <!-- Set sanitized version to the actual version suffix used in build/pack -->
-    <VersionSuffix>$(_VersionLabel)</VersionSuffix>
+    <VersionSuffix Condition="!$(VersionLabel.Contains('refs/tags/'))">$(_VersionLabel)</VersionSuffix>
+    <!-- Special case for tags, the label is actually the version. Backs compat since passed-in value overrides MSBuild-set one -->
+    <Version Condition="$(VersionLabel.Contains('refs/tags/'))">$(_VersionLabel)</Version>
   </PropertyGroup>
 
   <ItemGroup Label="ThisAssembly.Project">
@@ -142,6 +144,16 @@
     <ProjectProperty Include="PublicKeyToken" />
   </ItemGroup>
 
+  <ItemGroup Label="Throw">
+    <Using Include="System.ArgumentException" Static="true" />
+    <Using Include="System.ArgumentOutOfRangeException" Static="true" />
+    <Using Include="System.ArgumentNullException" Static="true" />
+  </ItemGroup>
+
   <Import Project="Directory.props" Condition="Exists('Directory.props')"/>
   <Import Project="Directory.props.user" Condition="Exists('Directory.props.user')" />
+
+  <!-- Implemented by SDK in .targets, guaranteeing it's overwritten. Added here since we add a DependsOnTargets to it. 
+       Covers backwards compatiblity with non-SDK projects. -->
+  <Target Name="InitializeSourceControlInformation" />
 </Project>
diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets
index 0cb1e4e..6232750 100644
--- a/src/Directory.Build.targets
+++ b/src/Directory.Build.targets
@@ -4,6 +4,13 @@
   <PropertyGroup Condition="'$(CI)' == 'true' and '$(Language)' == 'C#'">
     <DefineConstants>CI;$(DefineConstants)</DefineConstants>
   </PropertyGroup>
+
+  <PropertyGroup Label="Build">
+    <!-- Tests projects don't need API docs, typically -->
+    <GenerateDocumentationFile Condition="$(GenerateDocumentationFile) == '' and $(IsTestProject) == 'true'">false</GenerateDocumentationFile>
+    <GenerateDocumentationFile Condition="$(GenerateDocumentationFile) == '' and $(MSBuildProjectName.Contains('Tests'))">false</GenerateDocumentationFile>
+    <GenerateDocumentationFile Condition="$(GenerateDocumentationFile) == ''">true</GenerateDocumentationFile>
+  </PropertyGroup>
   
   <PropertyGroup Condition="'$(IsPackable)' == ''">
     <IsPackable Condition="'$(PackAsTool)' == 'true'">true</IsPackable>
@@ -27,23 +34,28 @@
   
   <ItemGroup Condition="'$(IsPackable)' == 'true'" Label="NuGet">
     <!-- This is compatible with nugetizer and SDK pack -->
+    <!-- Only difference is we don't copy either to output directory -->
 
     <!-- Project-level icon/readme will already be part of None items -->
     <None Update="@(None -> WithMetadataValue('Filename', 'icon'))" 
           Pack="true" PackagePath="%(Filename)%(Extension)" 
+          CopyToOutputDirectory="Never"
           Condition="'$(PackageIcon)' != ''" />
 
     <None Update="@(None -> WithMetadataValue('Filename', 'readme'))" 
           Pack="true" PackagePath="%(Filename)%(Extension)" 
+          CopyToOutputDirectory="Never"
           Condition="'$(PackReadme)' != 'false' and '$(PackageReadmeFile)' != ''" />
     
     <!-- src-level will need explicit inclusion -->
     <None Include="$(MSBuildThisFileDirectory)icon.png" Link="icon.png" Visible="false" 
           Pack="true" PackagePath="%(Filename)%(Extension)"
+          CopyToOutputDirectory="Never"
           Condition="Exists('$(MSBuildThisFileDirectory)icon.png') and !Exists('$(MSBuildProjectDirectory)\icon.png')" />
 
     <None Include="$(MSBuildThisFileDirectory)readme.md" Link="readme.md"  
           Pack="true" PackagePath="%(Filename)%(Extension)"
+          CopyToOutputDirectory="Never"
           Condition="'$(PackReadme)' != 'false' and Exists('$(MSBuildThisFileDirectory)readme.md') and !Exists('$(MSBuildProjectDirectory)\readme.md')" />
   </ItemGroup>
 
@@ -94,19 +106,17 @@
     <RepositoryBranch Condition="'$(RepositoryBranch)' == '' and '$(BUDDY_EXECUTION_BRANCH)' != ''">$(BUDDY_EXECUTION_BRANCH)</RepositoryBranch>
   </PropertyGroup>  
 
-  <PropertyGroup Condition="'$(EnableRexCodeGenerator)' == 'true'">
-    <!-- VSCode/Razor compatibility -->
-    <CoreCompileDependsOn>PrepareResources;$(CoreCompileDependsOn)</CoreCompileDependsOn>
+  <PropertyGroup>
+    <!-- Default to Just Works resources generation. See https://www.cazzulino.com/resources.html -->
+    <CoreCompileDependsOn>CoreResGen;$(CoreCompileDependsOn)</CoreCompileDependsOn>
   </PropertyGroup>
-  
+
   <ItemGroup>
     <!-- Consider the project out of date if any of these files changes -->
     <UpToDateCheck Include="@(None);@(Content);@(EmbeddedResource)" />
-    <!-- We'll typically use ThisAssembly.Strings instead of the built-in resource manager codegen -->
-    <EmbeddedResource Update="@(EmbeddedResource)"  Generator="" Condition="'$(EnableRexCodeGenerator)' != 'true'" />
-    <EmbeddedResource Update="@(EmbeddedResource)" Condition="'$(EnableRexCodeGenerator)' == 'true'">
+    <!-- Opt-in to typed resource generation by setting custom tool to MSBuild:Compile -->
+    <EmbeddedResource Update="@(EmbeddedResource -> WithMetadataValue('Generator', 'MSBuild:Compile'))" Type="Resx">
       <!-- Default to Just Works resources generation. See https://www.cazzulino.com/resources.html -->
-      <Generator>MSBuild:Compile</Generator>
       <StronglyTypedFileName>$(IntermediateOutputPath)\$([MSBuild]::ValueOrDefault('%(RelativeDir)', '').Replace('\', '.').Replace('/', '.'))%(Filename).g$(DefaultLanguageSourceExtension)</StronglyTypedFileName>
       <StronglyTypedLanguage>$(Language)</StronglyTypedLanguage>
       <StronglyTypedNamespace Condition="'%(RelativeDir)' == ''">$(RootNamespace)</StronglyTypedNamespace>