Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a1526d9
♻️ refactor skill documentation and evaluation framework
aicia-bot Aug 14, 2026
3fef81e
✅ add comprehensive testing and validation for skill
aicia-bot Aug 14, 2026
7cf3543
♻️ enhance runner selection and error handling in remote-test
aicia-bot Aug 14, 2026
47f4a56
💬 update repository documentation for skill changes
aicia-bot Aug 14, 2026
c54b526
♻️ document build-tooling preparation in dotnet-remote-testing skill
aicia-bot Aug 14, 2026
6618d7f
✨ add image provisioning to dotnet-remote-testing runner
aicia-bot Aug 14, 2026
5b4213e
💬 update changelog with build-tooling preparation details
aicia-bot Aug 14, 2026
b3f9b30
🔨 add sync-skill-install.ps1 script for skill synchronization
aicia-bot Aug 14, 2026
ad660c1
📚 update repo governance for skill synchronization
aicia-bot Aug 14, 2026
8d8003f
📝 clarify skill synchronization process in README
aicia-bot Aug 14, 2026
c5dd446
✨ make dotnet-remote-testing results faithful to local environment
aicia-bot Aug 14, 2026
f133ba7
💬 update changelog for v0.9.0
aicia-bot Aug 14, 2026
1352427
♻️ refactor sync-skill-install for host-tool distinction
aicia-bot Aug 15, 2026
db92094
🐛 preserve configured user in docker prepared images
aicia-bot Aug 15, 2026
e3e2bdc
📚 expand git-visual-squash-summary catalog entry
aicia-bot Aug 16, 2026
87f4a9d
♻️ clarify immediate-action contract in squash-summary skill
aicia-bot Aug 16, 2026
699dbaa
♻️ clarify immediate-action contract in squash-summary skill
aicia-bot Aug 16, 2026
6a2738f
🔨 add kebab-case validation to sync script
aicia-bot Aug 16, 2026
5ecc791
🔧 strengthen dotnet-test validator contract
aicia-bot Aug 17, 2026
d130f08
♻️ refactor dotnet-test skill with immediate-action lockdown
aicia-bot Aug 17, 2026
5318085
💬 update README for dotnet-test availability
aicia-bot Aug 17, 2026
adf42bf
🔀 merge branch 'v0.9.0/finalization' of https://github.com/codebeltne…
aicia-bot Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
726 changes: 359 additions & 367 deletions AGENTS.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ This is a minor release that adds three .NET skills — `dotnet-test`, `dotnet-r
- `dotnet-remote-testing` skill that runs .NET tests inside Docker using official `mcr.microsoft.com/dotnet/sdk` images, honoring an existing `testenvironments.json` as authoritative when present and otherwise deriving environments from Microsoft's live release index, while reporting WSL and SSH as unsupported instead of silently falling back to the host,
- `dotnet-remote-testing` deterministic runner `remote-test.cs` owning configuration discovery, release parsing, digest-pinned image resolution, isolated source staging, NuGet caching, execution, result parsing, distinct failure classification, and cleanup behind a single entry point, with a built-in `--self-test` alongside a PowerShell harness,
- Offline-safe release discovery for `dotnet-remote-testing`: successful release metadata is cached outside the repository so later runs work without network access, and the parameter form surfaces the exact runner-computed target as the recommended option,
- Build-tooling preparation in `dotnet-remote-testing`, probing the resolved image for the `git` that MinVer, Nerdbank.GitVersioning, GitInfo, and SourceLink invoke during `dotnet build`, and layering it on through a digest-addressed image cached outside the repository when the image lacks it, so a minimal runner image no longer fails a sound build with `MINVER1007` while the reported image and digest stay the resolved base,
- Git metadata in the staged workspace for `dotnet-remote-testing`, copying the repository's `.git` directory into the disposable staged copy and resolving a linked worktree's `gitdir:` pointer to the real directory, so version stamping, SourceLink, and any repository-root probe that walks up to a `.git` directory behave as they do on the host instead of silently resolving elsewhere and changing what the tests observe, with `--no-git-metadata` as an explicit, reported opt-out for a repository whose history dominates staging cost,
- `dotnet test`-shaped result reporting in `dotnet-remote-testing`, breaking results down per test assembly and target framework and reporting each failure with its fully-qualified name, target framework, elapsed time, assertion message, stack trace, and test-written output, reporting an infrastructure failure with its own phase's log rather than a tail of the whole run, and adding `--show-log` for the complete container log,
- `dotnet-segregated-assets` skill that migrates an ASP.NET Core application to serve deployed static content from Codebelt Static Content Provider (`codebeltnet/web-cdn-origin:2.0.0`) while `wwwroot` remains the authoring root, separating app-owned assets from shared CDN assets and preserving Razor Class Library, framework, and generated Static Web Assets,
- `dotnet-segregated-assets` deterministic runner `segregate-assets.cs` that inspects static-asset topology, classifies existing segregation state, escalates Blazor, Razor Class Library, scoped-CSS, and frontend-build risk instead of blindly excluding it, resolves Cuemon TagHelper package versions from the NuGet V3 service index at plan time, reports cache-busting interfaces and registrations without rewriting Razor or C# source, and proves the publish invariant through `verify --run-publish` against an isolated temp directory,
- Artifact-first container contract for `dotnet-segregated-assets` in which both application Dockerfiles package an already-published `artifacts/publish/` directory rather than compiling source, with the validator rejecting an SDK stage, a `dotnet build` or `dotnet publish` step, an `mcr.microsoft.com` runtime, or a missing artifact copy,
Expand Down
25 changes: 17 additions & 8 deletions README.md

Large diffs are not rendered by default.

178 changes: 178 additions & 0 deletions scripts/sync-skill-install.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
param(
[string[]]$Skill,
[switch]$VerifyOnly,
[switch]$Prune
)

$ErrorActionPreference = 'Stop'

Set-StrictMode -Version Latest

$utf8NoBom = [System.Text.UTF8Encoding]::new($false)
[Console]::InputEncoding = $utf8NoBom
[Console]::OutputEncoding = $utf8NoBom
$OutputEncoding = $utf8NoBom

# Build output is regenerated per location, so its hashes never match across installs. Comparing it
# would bury real drift under noise and push agents back to fragile per-file copying.
$excludePattern = '(^|/)(bin|obj)/'

function Get-RepoRoot {
return (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
}

# HostRoot is the tool's own directory. Its absence means the tool is not installed on this machine and
# there is genuinely nothing to sync; a missing skill directory *under* an installed tool is drift.
function Get-InstallRoot {
param([string]$SkillName)

$home_ = [Environment]::GetFolderPath('UserProfile')
return @(
[pscustomobject]@{ HostRoot = (Join-Path $home_ '.claude'); Path = (Join-Path $home_ ".claude/skills/$SkillName") },
[pscustomobject]@{ HostRoot = (Join-Path $home_ '.agents'); Path = (Join-Path $home_ ".agents/skills/$SkillName") },
[pscustomobject]@{ HostRoot = (Join-Path $home_ '.gemini/antigravity-cli'); Path = (Join-Path $home_ ".gemini/antigravity-cli/skills/$SkillName") }
)
}

function Get-RelativeFile {
param([string]$Root)

if (-not (Test-Path $Root)) {
return @()
}

$base = (Resolve-Path $Root).Path.TrimEnd('\', '/')
return @(Get-ChildItem -LiteralPath $Root -Recurse -File -Force |
ForEach-Object { $_.FullName.Substring($base.Length + 1).Replace('\', '/') } |
Where-Object { $_ -notmatch $excludePattern })
}

function Sync-SkillTree {
param(
[string]$SourceRoot,
[string]$InstallRoot,
[string[]]$RelativeFile
)

foreach ($rel in $RelativeFile) {
$destination = Join-Path $InstallRoot $rel
$destinationDir = Split-Path -Parent $destination
if (-not (Test-Path $destinationDir)) {
New-Item -ItemType Directory -Force -Path $destinationDir | Out-Null
}

Copy-Item -LiteralPath (Join-Path $SourceRoot $rel) -Destination $destination -Force
}
}

function Test-SkillTree {
param(
[string]$SourceRoot,
[string]$InstallRoot,
[string[]]$RelativeFile
)

$drift = @()

foreach ($rel in $RelativeFile) {
$expected = (Get-FileHash -LiteralPath (Join-Path $SourceRoot $rel) -Algorithm SHA256).Hash
$destination = Join-Path $InstallRoot $rel
$actual = if (Test-Path -LiteralPath $destination) {
(Get-FileHash -LiteralPath $destination -Algorithm SHA256).Hash
}
else {
'MISSING'
}

if ($actual -ne $expected) {
$drift += " DRIFT $rel"
}
}

# A rename or deletion in the repository leaves the old file behind in an install, where a stale
# skill keeps loading it. Extras are drift too, not cosmetic residue.
foreach ($rel in (Get-RelativeFile -Root $InstallRoot)) {
if ($RelativeFile -notcontains $rel) {
if ($Prune) {
Remove-Item -LiteralPath (Join-Path $InstallRoot $rel) -Force
}
else {
$drift += " EXTRA $rel"
}
}
}

return $drift
}

$repoRoot = Get-RepoRoot
$skillsRoot = Join-Path $repoRoot 'skills'

if (-not $Skill -or $Skill.Count -eq 0) {
$Skill = @(Get-ChildItem -LiteralPath $skillsRoot -Directory | ForEach-Object { $_.Name })
}

$totalDrift = 0

foreach ($name in $Skill) {
# Both roots are built by joining this name onto a trusted prefix, so a separator or `..` in it walks
# the sync out of the skill tree: the source becomes the repo and the install becomes the skills root,
# where -Prune would delete every other installed skill. Skill directories are kebab-case by
# convention, so anything else is malformed input rather than a skill that is merely missing.
if ($name -notmatch '^[a-z0-9]+(-[a-z0-9]+)*$') {
throw "Invalid skill name: '$name'. Expected a kebab-case skill directory name."
}

$sourceRoot = Join-Path $skillsRoot $name
if (-not (Test-Path -LiteralPath $sourceRoot)) {
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Write-Host "[SKIP] $name (not a repo-managed skill)"
continue
}

$relativeFile = Get-RelativeFile -Root $sourceRoot

foreach ($install in (Get-InstallRoot -SkillName $name)) {
$installRoot = $install.Path

if (-not (Test-Path -LiteralPath $install.HostRoot)) {
Write-Host "[SKIP] $name -> $installRoot (host tool not installed)"
continue
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

# An installed host with no copy of the skill used to be skipped, which let a run where nothing
# was ever installed still report "verified, 0 drift". A sync creates the install; a verify fails.
if (-not (Test-Path -LiteralPath $installRoot)) {
if ($VerifyOnly) {
Write-Host "[FAIL] $name -> $installRoot (not installed)"
$totalDrift += 1
continue
}

New-Item -ItemType Directory -Force -Path $installRoot | Out-Null
}

if (-not $VerifyOnly) {
Sync-SkillTree -SourceRoot $sourceRoot -InstallRoot $installRoot -RelativeFile $relativeFile
}

$drift = @(Test-SkillTree -SourceRoot $sourceRoot -InstallRoot $installRoot -RelativeFile $relativeFile)
$totalDrift += $drift.Count

if ($drift.Count -eq 0) {
Write-Host "[PASS] $name -> $installRoot ($($relativeFile.Count) files identical)"
}
else {
Write-Host "[FAIL] $name -> $installRoot ($($drift.Count) drifted)"
$drift | ForEach-Object { Write-Host $_ }
}
}
}

Write-Host ''
if ($totalDrift -eq 0) {
Write-Host "Install sync: verified, 0 drift."
exit 0
}

Write-Host "Install sync: $totalDrift drifted entr$(if ($totalDrift -eq 1) { 'y' } else { 'ies' })."
exit 1
17 changes: 15 additions & 2 deletions scripts/validate-skill-templates.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,15 @@ Add-ValidationResult -Results $results -Name 'dotnet-test encodes role-specific
Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'zero remaining `WebApplicationFactory`'
Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'Do not invent an endpoint, service, configuration key, or expected result.'
Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'An MTP executable run may supplement that gate but never replaces it'
# The skill once answered a bare invocation with a capability menu and inspected nothing; these lock the evidence-first contract.
Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'You were invoked. That is the request.'
Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'Forbidden as a first response:'
Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'The test host comes from Codebelt, not from Microsoft'
Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'it is the fallback for genuine ambiguity, not an intake wizard'
Assert-Contains -Name 'dotnet-test/SKILL.md' -Content $skill -Needle 'do not exist below Codebelt xUnit **11.1.0**'
Assert-Contains -Name 'dotnet-test/FORMS.md' -Content $forms -Needle 'This form is a fallback for genuine ambiguity, not an intake step.'
Assert-Contains -Name 'inspect-dotnet-tests.ps1' -Content $inspect -Needle '$managedFixtureFloor = [version]'
Assert-Contains -Name 'dotnet-test/web-functional-tests.md' -Content $web -Needle 'Probing with `if (_application is IAsyncDisposable d)` is dead defensive code'
Assert-Contains -Name 'dotnet-test/FORMS.md' -Content $forms -Needle '### project_selection'
Assert-Contains -Name 'dotnet-test/FORMS.md' -Content $forms -Needle '### operation_mode'
Assert-Contains -Name 'dotnet-test/FORMS.md' -Content $forms -Needle '### test_role'
Expand Down Expand Up @@ -1280,12 +1289,16 @@ Add-ValidationResult -Results $results -Name 'dotnet-test encodes role-specific
Assert-Contains -Name 'test-resolve-test-package-versions.ps1' -Content $resolveTest -Needle 'restore evidence'

$evalObject = $evals | ConvertFrom-Json
if (@($evalObject.evals).Count -ne 6) {
throw "dotnet-test must define exactly six requested paired eval scenarios; found $(@($evalObject.evals).Count)"
if (@($evalObject.evals).Count -lt 7) {
throw "dotnet-test must define the six paired role scenarios plus the bare-invocation immediate-action scenario; found $(@($evalObject.evals).Count)"
}
foreach ($needle in @('attached Acme.Calculator fixture', 'xUnit v2 project', 'web-cdn-origin-style', 'IClassFixture<WebApplicationFactory<Program>>', 'ApplicationTestFactory pattern', 'ApplicationTest<Program, ManagedApplicationFixture<Program>>')) {
Assert-Contains -Name 'dotnet-test/evals/evals.json' -Content $evals -Needle $needle
}
# A bare invocation must act on inspector evidence instead of answering with a menu; that regression shipped once, so it stays covered.
foreach ($needle in @('Does not present a numbered menu of modes', 'Runs inspect-dotnet-tests.ps1 as the first action')) {
Assert-Contains -Name 'dotnet-test/evals/evals.json' -Content $evals -Needle $needle
}
if (@($fixtureFiles | Where-Object { $_ -match '(^|[\\/])(bin|obj)([\\/]|$)' }).Count -gt 0) {
throw 'dotnet-test eval fixtures must not include bin/ or obj/ paths'
}
Expand Down
37 changes: 25 additions & 12 deletions skills/dotnet-remote-testing/FORMS.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
# .NET Remote Testing Input Form

Collect only the fields that are still unresolved after inspecting the request and the repository. Most remote-test requests are fully determined and need **no** questions — for example, "remote test this solution" against a repository with a single applicable environment. Prefer native structured controls when the host provides them; otherwise use the plain-text fallback below without changing field order, defaults, or the final confirmation.
This form is a **fallback for genuine ambiguity, not an intake checklist**. The default path collects nothing: a request to remote test is executed, not surveyed.

## Autonomy gate — evaluate before presenting any field

Present a field only when one of these is true:

1. The runner exited `SelectionRequired` (`16`) — present `environment`, restricted to the `candidates` it returned.
2. The developer explicitly asked to choose something ("let me pick the environment", "which options do I have?").
3. The developer supplied a value that is genuinely unusable (for example a project path that does not exist).

If none apply, run with the defaults — auto-resolved target, `Debug`, no coverage — and present **no** fields and **no** confirmation. A single applicable Docker environment in `testenvironments.json` is a resolved answer, not a question. Never walk the field list top-to-bottom to "gather requirements", and never ask `test_scope`, `configuration`, or `coverage` unprompted; those are defaults the developer overrides by saying so.

Prefer native structured controls when the host provides them; otherwise use the plain-text fallback below without changing field order, defaults, or the final confirmation.

## Fields

### environment

- **type:** single-choice
- **prompt:** Which environment should run the tests?
- **choices:** Dynamically list the environments from `remote-test.cs list` — the configured Docker environments from `testenvironments.json`, or the Microsoft-derived environments (for example `dotnet-10-lts`, `dotnet-9-sts`, `dotnet-11-preview`) when no `testenvironments.json` exists
- **default:** The only applicable Docker environment, or the environment explicitly named by the user (Recommended)
- **required:** true
- **choices:** The `candidates` returned by the runner's `SelectionRequired` result — the configured Docker environments from `testenvironments.json`, or the Microsoft-derived environments (for example `dotnet-10-lts`, `dotnet-9-sts`, `dotnet-11-preview`) when no `testenvironments.json` exists
- **default:** The environment explicitly named by the user
- **required:** Only when the runner exits `SelectionRequired`. A single applicable Docker environment resolves automatically and is never asked.

### test_scope

Expand All @@ -21,7 +33,7 @@ Collect only the fields that are still unresolved after inspecting the request a
- A specific project
- A class or test filter
- **default:** Entire solution / auto-resolved target (Recommended)
- **required:** true
- **required:** false — the default applies silently; ask only when the developer asks to narrow the run but does not say how

### project

Expand Down Expand Up @@ -50,7 +62,7 @@ Collect only the fields that are still unresolved after inspecting the request a
- Debug (Recommended)
- Release
- **default:** Debug (Recommended)
- **required:** true
- **required:** false — `Debug` applies silently unless the developer names a configuration

### coverage

Expand All @@ -60,7 +72,7 @@ Collect only the fields that are still unresolved after inspecting the request a
- No (Recommended)
- Yes
- **default:** No (Recommended)
- **required:** true
- **required:** false — never ask; coverage is collected only when the developer requests it

### confirmation

Expand All @@ -70,15 +82,16 @@ Collect only the fields that are still unresolved after inspecting the request a
- Yes (Recommended)
- No
- **default:** Yes (Recommended)
- **required:** true
- **required:** Only when at least one other field was presented. On the autonomous path there is nothing to confirm — the run is the answer.

## Presentation rules

- Infer explicit answers from the request and from `remote-test.cs list`/`plan`; do not ask them again.
- Ask one unresolved field at a time. Never bundle multiple questions.
- Clear the autonomy gate above before presenting anything. In practice most invocations present no fields at all.
- Infer explicit answers from the request and from the runner's own output; do not ask them again.
- Ask one unresolved field at a time. Never bundle multiple questions, and never turn a single blocking choice into a broader intake.
- Present the recommended/default choice first and suffix it with `(Recommended)`.
- For the `environment` field, offer the discovered environment names as selectable choices rather than free text. When exactly one environment applies, select it without asking.
- For the `environment` field, offer the runner's `candidates` as selectable choices rather than free text. When exactly one environment applies, select it without asking.
- For `project`, offer the auto-resolved target as a selectable choice alongside a free-text path.
- In plain-text fallback mode, start immediately with `Field: <field-name>` and show numbered choices. Do not add a conversational preamble.
- If the user leaves a shown computed/default choice blank, accept it and continue.
- After all fields are resolved, summarize the exact environment, target, configuration, and coverage, then ask `confirmation`.
- When fields were presented, summarize the exact environment, target, configuration, and coverage after they are resolved, then ask `confirmation`. When no field was presented, skip the summary and the confirmation and run.
Loading
Loading