diff --git a/AGENTS.md b/AGENTS.md index 80d4201..18aa203 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ Where `skill-creator` says to spawn with-skill and baseline subagents in the sam ### Asking for an eval -`eval `, `evaluate `, `eval this skill`, `prepare evals for `, and `evaluate using the existing evals` are all requests for this workflow. Treat them as instructions to prepare the package, never to run it, and never as a request to write new eval cases unless the user asks for that too. +`eval `, `evaluate `, `eval this skill`, `prepare evals for `, `evaluate using the existing evals`, `please do an eval`, and similar explicit evaluation requests all invoke this workflow. Treat them as instructions to prepare the package, never to run it, and never as a request to write new eval cases unless the user asks for that too. Resolve the execution configuration before running the package preparation script. Normalize explicit user intent immediately and do not ask again for a harness the user already supplied: `Codex` -> `codex`; `GitHub Copilot`, `GitHub Copilot CLI`, or `Copilot` -> `github-copilot`; `OpenCode` -> `opencode`; matching is case-insensitive. In an interactive agent session, offer Codebelt Reference first (`GitHub Copilot CLI` + `claude-haiku-4.5`) and let `scripts/prepare-skill-evals.ps1` validate that model internally; if it is unavailable, show the current discovered Copilot models and ask for a replacement. If the user selects Codex without a model, pass `-Runner codex` and let preparation resolve repository defaults (`gpt-5.6-luna` with low reasoning) and validate the model. For manual selection, ask for Harness only when the user did not already name one, then discover current models for that harness with `scripts/Get-HarnessModels.ps1`. OpenCode discovery mirrors every model exposed by all configured OpenCode providers; it exposes exact `provider/model` selectors and retains display and availability metadata when available, but availability is presentation metadata only and never filters the selectable catalog. If OpenCode is selected and no model was explicitly supplied, present every discovered selector to the user, ask the user to choose one, and stop until that choice is made. Do not choose the first, free, recommended, previous-iteration, previous-successful, or previous-failed model on the user's behalf. If the user explicitly supplies an OpenCode selector, preserve it verbatim in `execution-profile.json`; discovery may verify or annotate it, but discovery failure or incomplete metadata must never substitute another model. GitHub Copilot and Codex discovery lists all currently available models. Never guess stale model ids, silently switch harnesses, or generate an execution-ready package with a null runner or model. @@ -91,21 +91,13 @@ Useful switches: `-Eval ` to prepare a subset, `-Iteration ` plus `-Fo The expected output and the assertions are the grading key. They belong in `eval-metadata.json`, outside every run directory, and must never appear in either prompt — a baseline handed the answer key is not a baseline. -### Eval preparation is a completion gate +### Optional eval preparation -Adding or modifying any repo-managed skill triggers this workflow. It is not something the user asks for separately, and "the change is small" or "the evals did not change" does not exempt it. Touching `SKILL.md`, `FORMS.md`, `references/`, `scripts/`, `assets/`, or `evals/` under `skills//` is a skill change. +Eval package preparation is optional. A skill change does not require package preparation, and it does not require choosing a harness or model. Prepare a package only when the user explicitly asks for an evaluation, such as `eval `, `evaluate `, `please do an eval`, or a similar request. A request to create, modify, validate, or release a skill does not implicitly ask for eval preparation. -After the final skill edit is in place, run: +When the user explicitly asks for an eval, follow [Asking for an eval](#asking-for-an-eval), resolve the Harness + Model choice, and run the preparation command described there. Preparation remains model-free and never executes the generated prompts. If a package is prepared, name its prompt path in the completion message and hand it over as described below. Do not ask for a harness, prepare a package, or report an eval handoff solely because a skill changed. -``` -pwsh -NoProfile -NonInteractive -File ./scripts/prepare-skill-evals.ps1 -Changed -Runner -Model -``` - -It resolves every repo-managed skill this branch changed, uncommitted work included, and prepares a package for each. With no skill changed it says so and exits clean, which satisfies the gate. - -Then name the prepared prompt paths in the completion message so the user knows what is waiting for them. Preparing and reporting satisfies this gate. Executing a prompt never does, and an agent that runs one has broken the Priority 1 rule rather than completed the gate. - -Run it before `scripts/sync-skill-install.ps1`, which stays the last gate because it must observe the final state of every file. See [Blocking Completion Gates](#blocking-completion-gates). +`scripts/sync-skill-install.ps1` remains the last gate when a repo-managed skill was edited. An explicitly requested eval adds package preparation before handoff, but package preparation is not a completion gate. See [Blocking Completion Gates](#blocking-completion-gates). ### Manual execution boundary @@ -181,13 +173,15 @@ Every repo-managed skill must include its own `evals/evals.json` file at `skills - When `files` is present, keep the paths relative to `skills//` and validate that every fixture exists - Treat eval prompts, expected outcomes, and assertions as versioned review specifications; their presence never authorizes automated model execution - Start with `pwsh -NoProfile -File ./scripts/validate-skill-templates.ps1 -MetadataOnly` for a sub-second repository-wide metadata and fixture check -- Run only the changed skill's deterministic validator and focused regression scripts during iteration; independent read-only checks may use bounded local parallelism, while shared-file mutations stay sequential -- Run `pwsh -NoProfile -File ./scripts/validate-skill-templates.ps1` once before completion for the repository gate +- Run only the changed skill's deterministic validator and focused regression scripts during iteration; run independent checks concurrently with bounded local parallelism, while shared-file mutations stay sequential. +- For the repository completion gate, run the independent suites from `.github/workflows/validate-skill-templates.yml` concurrently. Use that matrix as the source of truth for script paths and `-Suite` values, including the transport-specific conformance suites and phase-specific integrity suites. Use separate PowerShell 7 processes, bounded concurrency with at least two workers, isolated temporary fixtures, and per-suite exit codes. Include `-Full` for DocFX when required by the task. Every required suite must pass. +- Do not invoke the sequential aggregate `scripts/validate-skill-templates.ps1` with no `-Suite` or with `-Suite All` for agent-driven validation. Do not run the CI suite matrix in a sequential loop. If concurrent execution is unavailable, report the limitation instead of silently falling back to sequential execution. +- An explicit user instruction to skip or stop testing overrides these validation gates for that session. Stop any tests you started, run no further validation, and report the unvalidated work. - Follow the top-level **AI/LLM Evaluation Automation Prohibition** for every eval. No per-skill or third-party requirement overrides it. - To compare a skill against a baseline, prepare a package with **Portable Eval Handoff** and hand `RUN-THIS.prompt.md` to the user; the repository agent never runs the prompts, while the user-directed external executor runs, grades, and reports the paired comparison - Deterministic scaffold/template skills must keep local deterministic validators as well; evals supplement validators, they do not replace them -If you add a new skill or modify an existing repo-managed skill, update that skill's `evals/evals.json` and run `pwsh -NoProfile -NonInteractive -File ./scripts/prepare-skill-evals.ps1 -Changed -Runner -Model ` before considering the work complete. Use `-CodebeltReference` instead only after its dynamic Copilot model check passes. Do not commit temp workspaces, benchmark outputs, or generated review files into this repository unless the user explicitly asks for checked-in artifacts. +If you add a new skill or modify an existing repo-managed skill, keep that skill's `evals/evals.json` present and update it when the eval definitions or fixtures change. Package preparation is optional and is triggered only by an explicit eval request; when requested, use `pwsh -NoProfile -NonInteractive -File ./scripts/prepare-skill-evals.ps1 -Changed -Runner -Model `. Use `-CodebeltReference` instead only after its dynamic Copilot model check passes. Do not commit temp workspaces, benchmark outputs, or generated review files into this repository unless the user explicitly asks for checked-in artifacts. ## Git Identity @@ -317,7 +311,7 @@ Before any completion message, reread the skill instructions and the current con For script-backed workflows, creating or editing files is not enough on its own. If a skill requires deterministic maintenance or verification commands, run them before completion and report their concrete outcome. For `dotnet-docfx-digest`, `scripts/agents.cs` and `scripts/docfx.cs --build-api-model --validate-samples --verify-docfx-build` are blocking completion gates whenever the skill or task summary says they are required. -Whenever a repo-managed skill was edited, two gates apply in a fixed order. `pwsh -NoProfile -NonInteractive -File ./scripts/prepare-skill-evals.ps1 -Changed -Runner -Model ` (or `-CodebeltReference` after dynamic availability verification) runs first and prepares the eval packages for the changed skills, reporting the prompt paths. `scripts/sync-skill-install.ps1` runs last, because every other step can still change a file. Report the actual output of both; an earlier run in the same session satisfies neither. See [Eval preparation is a completion gate](#eval-preparation-is-a-completion-gate) and [Local Install Sync](#local-install-sync). +When a repo-managed skill was edited, `scripts/sync-skill-install.ps1` runs last, because every other step can still change a file. Report its actual output; an earlier run in the same session satisfies neither. If the user explicitly requested an eval, prepare the package before handing it over as described in [Optional eval preparation](#optional-eval-preparation), but do not treat that preparation as a completion gate. See [Local Install Sync](#local-install-sync). ## User Input UX diff --git a/CHANGELOG.md b/CHANGELOG.md index 21f0132..49cb321 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.10.0] - 2026-09-11 + +This is a minor release that introduces `dotnet-nuget-update`, a deterministic NuGet dependency audit and update workflow for .NET repositories. The skill owns the complete-audit invariant, ensuring every declared package version is accounted for before any update is applied. It handles both central package management (`Directory.Packages.props`) and project-level `PackageReference` versioning, tracks stable and prerelease intent, preserves TFM-band pins (keeping `net9` or `net10` packages within their matching major when that major is the compatibility signal), and supports both normal mode (auto-applies patch/minor/revision, batches majors for approval) and yolo mode (auto-applies safe classes only, reports held majors). All scripts are deterministic and offline-testable via bundled fixtures. Alongside that, `git-visual-commits` gained refactoring and three new eval cases for edge-case coverage, `git-keep-a-changelog` gained improved entity-classification infrastructure and a dedicated reference document for section validation, and repository-level validation enhancements ensure all skills conform to updated testing requirements. + +### Added + +- `dotnet-nuget-update` skill for auditing and updating NuGet dependencies with complete declaration accounting, supporting both central package management and project-level versioning, two interactive modes (normal with approval batching, yolo for safe updates only), stable/prerelease intent inference, and TFM-band awareness so conditional `net9`/`net10` package declarations stay within their matching major when that major is the compatibility signal rather than jumping to the newest overall release, +- bundled deterministic scripts for `dotnet-nuget-update`: `Get-DependencyAudit.ps1` for complete graph enumeration before any edit, `Get-PackageGraph.ps1` for central-package condition resolution, `Get-TargetFrameworks.ps1` for TFM matrix discovery, `Resolve-NuGetVersion.ps1` and `Compare-Version.ps1` for version investigation, `Apply-PackageUpdates.ps1` for minimal structural XML edits preserving comments and line endings, and `Get-NuGetSources.ps1` for feed configuration visibility, +- comprehensive test coverage for `dotnet-nuget-update`: regression harnesses for dependency audit, package graph, TFM-band logic, project-level package references, version comparison, and update application, together with offline-testable fixtures covering central-package scenarios, mixed stable/prerelease intent, multi-TFM bands, plain project references, and XML comment pinning, +- per-process memoization for live or offline flat-container NuGet version feeds in `dotnet-nuget-update`, with filesystem flat-container fixtures supporting deterministic offline testing, +- complete-audit invariant validation in `dotnet-nuget-update` ensuring `current + auto + approval + unresolved == declared` before reporting the repository as updated, +- history-aware update tracking in `dotnet-nuget-update` with adjacent XML comments surfacing as `note` fields so pin rationale remains visible during audits and auto-updates marked with `READ THE NOTE before applying` receive explicit attention, +- README catalog entry, installation snippet, and "Why dotnet-nuget-update?" community health section explaining the need for complete dependency graph auditing, TFM-band awareness, and preservation of intentional pins and compatibility markers, +- `references/section-validation.md` in `git-keep-a-changelog` documenting the section-classification contract for resolved release entities, +- test harness `scripts/test-resolve-release-entity.ps1` in `git-keep-a-changelog` validating entity-classification behavior across multiple release scenarios. + +### Changed + +- `git-visual-commits` SKILL.md refactored for improved clarity, with single-category quality-gate guidance hardened and commit-language best practices restructured to emphasize inventory refresh and per-path explanation before classification, +- `git-visual-commits` evals expanded with three new eval cases (26, 27, 28) covering untracked file discovery, path handling with spaces and untracked directories, and explicit scope narrowing, +- `git-keep-a-changelog` SKILL.md improved with clarifications on entity classification workflow, pending-change handling, and section-validation procedures, +- `git-keep-a-changelog` evals updated to reflect improved entity-classification guidance and concurrent validation requirements, +- Repository validation infrastructure in `scripts/validate-skill-templates.ps1` hardened with concurrent suite dispatch, bounded concurrency of at least two workers, per-suite exit codes, and explicit prohibition on sequential aggregate runs, +- Repository agent guidelines in `AGENTS.md` clarified with explicit concurrent execution guidance, per-suite validation, and user-instruction overrides for testing suspension, +- README.md documentation updated with concurrent validation procedures and explicit messaging about running independent validation suites in parallel with bounded concurrency. + ## [0.9.1] - 2026-09-07 This patch release adds harness-agnostic Eval Runner execution boundary infrastructure without changing the paired evaluation methodology or existing report schemas, while optimizing skill descriptions and refactoring repository-level authoring guidance. Prepared packages now carry `execution-profile.json`, package-local runner protocol tools, and normalized `execution-result.json` evidence. The deterministic fake runner is the conformance reference, with Codex, GitHub Copilot CLI, and OpenCode as supported real adapters. Repository automation remains model-free; only a human-directed external Eval Orchestrator may invoke the selected runner, and unsupported isolation fails closed. @@ -638,6 +664,7 @@ This is a minor release that introduces two complementary git workflow skills, e - Improved scaffold fidelity with hidden `.bot` asset preservation, explicit UTF-8 and BOM handling, and checks aimed at preventing mojibake or incomplete generated output. +[0.10.0]: https://github.com/codebeltnet/agentic/compare/v0.9.1...v0.10.0 [0.9.1]: https://github.com/codebeltnet/agentic/compare/v0.9.0...v0.9.1 [0.9.0]: https://github.com/codebeltnet/agentic/compare/v0.8.2...v0.9.0 [0.8.2]: https://github.com/codebeltnet/agentic/compare/v0.8.1...v0.8.2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8df4768..385f52f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,13 +86,15 @@ Evals let you verify the skill works and measure improvement over a baseline. Ev Aim for 3–5 evals that cover distinct scenarios: happy path, edge cases, and cases where the skill should *not* do something. -Evals are prepared, not executed, from this repository. Adding or modifying a repo-managed skill requires preparing the packages for every skill the branch touched, which is a completion gate rather than an optional extra: +Evals are prepared, not executed, from this repository. Package preparation is optional and happens only after an explicit eval request, such as `eval `, `evaluate `, or `please do an eval`. Adding or modifying a repo-managed skill does not require a package, a harness choice, or a model choice. + +When an eval is requested, prepare the changed packages with: ```console pwsh -NoProfile -NonInteractive -File ./scripts/prepare-skill-evals.ps1 -Changed -Runner github-copilot ``` -Run it after the last skill edit and before `scripts/sync-skill-install.ps1`, which stays last. For a single skill on demand, use: +For a single skill on demand, use: ```console pwsh -NoProfile -NonInteractive -File ./scripts/prepare-skill-evals.ps1 -Skill -Runner -Model @@ -149,7 +151,7 @@ pwsh -NoProfile -File ./scripts/validate-skill-templates.ps1 -Ref HEAD - [ ] At least one eval in `evals/evals.json` - [ ] The skill's `evals/evals.json` exists and its `skill_name` matches the folder/frontmatter name - [ ] Any optional `files` entries in `evals/evals.json` point to real fixture files under the same skill folder -- [ ] `pwsh -NoProfile -NonInteractive -File ./scripts/prepare-skill-evals.ps1 -Changed -Runner -Model ` or `-CodebeltReference` was run after the last skill edit, and the prepared prompt paths were reported +- [ ] If an eval was explicitly requested, `pwsh -NoProfile -NonInteractive -File ./scripts/prepare-skill-evals.ps1 -Changed -Runner -Model ` or `-CodebeltReference` was run, and the prepared prompt paths were reported - [ ] If an external evaluation was run, each result includes the producing model and the package contains the first-party `report.html`, exact upstream `skill-creator-report.html`, `benchmark.json`, and `benchmark.md`; use `-CollectResults` only for explicitly authorized forensic recovery of an existing package - [ ] `scripts/validate-skill-templates.ps1` passes for the current working tree when changing scaffold or template behavior - [ ] If CI is enabled for the branch, the GitHub Actions validation job passes too diff --git a/README.md b/README.md index b2dce65..7fd7b2b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Another repo rule is intentionally strict: every repo-managed skill ships with i Skill validation is local and deterministic. The Priority 1 **AI/LLM Evaluation Automation Prohibition** in `AGENTS.md` forbids repository preparation, validation, CI, hooks, deterministic tests, automatic fan-out, graders, and completion gates from using an authenticated Copilot, Claude, Codex, Gemini, or other model account. There is no repository opt-in switch. A human-selected external Eval Orchestrator may invoke an explicitly selected package-local Eval Runner for a package it was handed; that boundary never authorizes live model execution in CI or automatic repository workflows. -Evaluation keeps Anthropic's `skill-creator` methodology and portable paired-run conventions while replacing only the execution transport. `run.json` remains the runner-neutral one-arm contract; `execution-profile.json` selects the runner/model/configuration; and `delegation.dispatch_owner` declares whether the orchestrator dispatches a native subagent/task or starts the runner-owned native execution surface directly. Orchestrator-owned envelopes pass through `record-native-result.ps1`; runner-owned transports produce `execution-result.json` directly before the existing `eval-result/2` bridge and reports. The user-facing preparation flow normalizes explicit harness intent before emitting `RUN-THIS.prompt.md`; the portable profile stores the internal runner id and the opaque runner-native model selector, with no provider field. The conforming real runners are GitHub Copilot, Codex, and OpenCode, with a deterministic fake runner used for conformance. GitHub Copilot CLI with `claude-haiku-4.5` is the Codebelt Reference evaluation configuration — a repository convention for economical, stable comparison, not an Anthropic default — and Codex defaults to `gpt-5.6-luna` with low reasoning. Package preparation validates the resolved model against the current harness catalog before writing the profile. OpenCode discovery mirrors every model exposed by all configured providers, preserves exact `provider/model` selectors, and retains availability only as presentation metadata; it never filters paid or unknown models. When OpenCode is selected without an explicit model, the user must choose from the discovered selectors before package preparation, and no first/free/recommended/previous model may be selected automatically. GitHub Copilot and Codex discovery lists all currently available models. The repository prepares the paired candidate and baseline inputs as a portable package and stops; the external Eval Orchestrator resolves, preflights, follows the selected dispatch owner, then grades and reports. Runners grant full operational permission inside each isolated behavioral harness configuration so the evaluated agent can perform ordinary engineering work noninteractively. Hard filesystem confinement is a separate optional outer capability: it raises reported isolation confidence from pragmatic to strict when proven, but it is not a universal platform prerequisite, so Windows is a first-class pragmatic target; mandatory experimental controls remain fail-closed. Native skill activation is not evaluated in v0.9.1. Adding or modifying a skill triggers package preparation automatically, as a completion gate an agent cannot skip: +Evaluation keeps Anthropic's `skill-creator` methodology and portable paired-run conventions while replacing only the execution transport. `run.json` remains the runner-neutral one-arm contract; `execution-profile.json` selects the runner/model/configuration; and `delegation.dispatch_owner` declares whether the orchestrator dispatches a native subagent/task or starts the runner-owned native execution surface directly. Orchestrator-owned envelopes pass through `record-native-result.ps1`; runner-owned transports produce `execution-result.json` directly before the existing `eval-result/2` bridge and reports. The user-facing preparation flow normalizes explicit harness intent before emitting `RUN-THIS.prompt.md`; the portable profile stores the internal runner id and the opaque runner-native model selector, with no provider field. The conforming real runners are GitHub Copilot, Codex, and OpenCode, with a deterministic fake runner used for conformance. GitHub Copilot CLI with `claude-haiku-4.5` is the Codebelt Reference evaluation configuration — a repository convention for economical, stable comparison, not an Anthropic default — and Codex defaults to `gpt-5.6-luna` with low reasoning. Package preparation validates the resolved model against the current harness catalog before writing the profile. OpenCode discovery mirrors every model exposed by all configured providers, preserves exact `provider/model` selectors, and retains availability only as presentation metadata; it never filters paid or unknown models. When OpenCode is selected without an explicit model, the user must choose from the discovered selectors before package preparation, and no first/free/recommended/previous model may be selected automatically. GitHub Copilot and Codex discovery lists all currently available models. The repository prepares the paired candidate and baseline inputs as a portable package and stops; the external Eval Orchestrator resolves, preflights, follows the selected dispatch owner, then grades and reports. Runners grant full operational permission inside each isolated behavioral harness configuration so the evaluated agent can perform ordinary engineering work noninteractively. Hard filesystem confinement is a separate optional outer capability: it raises reported isolation confidence from pragmatic to strict when proven, but it is not a universal platform prerequisite, so Windows is a first-class pragmatic target; mandatory experimental controls remain fail-closed. Native skill activation is not evaluated in v0.9.1. Package preparation is optional and happens only when the user explicitly asks for an evaluation, such as `eval `, `evaluate `, or `please do an eval`; changing a skill alone does not require choosing a harness or preparing a package: Phase 1 closes by writing an immutable `execution-freeze.json` ledger with the exact manifest result paths and hashes of every runner-produced execution result and referenced raw transcript/event artifact. The bridge, grading application, and report adapter validate that ledger and never re-bless changed bytes. The external Grader writes only package-root `grading.json` (`codebeltnet/agentic/eval-grading/1`); `apply-eval-grading.ps1` projects only `passed` and `evidence` decisions onto canonical results. `finalize-eval-package.ps1` owns the deterministic completion boundary and succeeds only after validating the freeze, bridge, complete grading, and all four report artifacts. A changed raw file requires a fresh Phase 1 execution, and prose cannot substitute for finalizer success. Optional scripted `interaction.json` sidecars provide deterministic same-session user turns only when the selected runner advertises and preflights that capability; ordinary single-turn runs remain unchanged. @@ -56,9 +56,9 @@ Use the metadata-only mode for the fastest feedback on every skill manifest, fix pwsh -NoProfile -File ./scripts/validate-skill-templates.ps1 -MetadataOnly ``` -During iteration, run the changed skill's bundled deterministic validator and focused regression scripts. Before completion, run `pwsh -NoProfile -File ./scripts/validate-skill-templates.ps1`; use `-Full` when the slower DocFX suites are relevant. GitHub Actions supplies the same deterministic safety net. This layered path catches structural and behavioral regressions quickly without hidden model traffic. +During iteration, run the changed skill's bundled deterministic validator and focused regression scripts, overlapping independent checks. For the repository completion gate, agents must dispatch the suite entries in `.github/workflows/validate-skill-templates.yml` concurrently in separate PowerShell 7 processes with bounded concurrency of at least two workers, isolated temporary fixtures, and per-suite exit codes. Use `-Full` when the DocFX suites are relevant. Sequential aggregate runs and sequential loops over the matrix are not permitted for agent-driven validation. If concurrent execution is unavailable, report the limitation. An explicit user instruction to skip or stop testing suspends validation for that session; report the work as unvalidated. -CI runs the complete validation set in 13 independent jobs with a five-minute budget per job. Template checks, package preparation, runner regressions, and DocFX have separate jobs; runner conformance is split by transport and integrity tests by phase. Every job uses isolated temporary fixtures, and the existing `validate-skill-templates` required check passes only when every job succeeds. New commits cancel obsolete runs for the same pull request. Each job records its elapsed time in the Actions summary. Local validation still defaults to all checks; `-Suite Templates`, `-Suite Preparation`, or `-Suite Runners` selects a focused group, and `-Full -Suite Docfx` selects DocFX. The conformance and integrity test scripts also accept the `-Suite` values listed in the workflow. This partition changes scheduling, not the model-free validation contract. +CI runs the complete validation set in 13 independent jobs with a five-minute budget per job. Template checks, package preparation, runner regressions, and DocFX have separate jobs; runner conformance is split by transport and integrity tests by phase. Every job uses isolated temporary fixtures, and the existing `validate-skill-templates` required check passes only when every job succeeds. New commits cancel obsolete runs for the same pull request. Each job records its elapsed time in the Actions summary. For focused local validation, `-Suite Templates`, `-Suite Preparation`, or `-Suite Runners` selects a group, and `-Full -Suite Docfx` selects DocFX. The conformance and integrity test scripts also accept the `-Suite` values listed in the workflow. Agents use these explicit suite selections for concurrent dispatch rather than the script's sequential default. This partition changes scheduling, not the model-free validation contract. ## Install a skill @@ -145,6 +145,7 @@ Each `SKILL.md` description is lean activation metadata. The catalog below expla | [dotnet-strong-name-signing](skills/dotnet-strong-name-signing/SKILL.md) | Generate a strong name key (`.snk`) file for signing .NET assemblies using pure .NET cryptography — no Visual Studio Developer PowerShell or `sn.exe` required. Works in any terminal. Defaults to 1024-bit RSA (matching `sn.exe`), with 2048 and 4096 available as options. | | [git-remote-release](skills/git-remote-release/SKILL.md) | Generate GitHub release notes by summarizing all commits and pull requests between two Git tags or branches in a remote GitHub repository. Accepts a compare URL or separate owner/repo, previous ref, and current ref values; falls back to comparing the current branch against the upstream default branch when no input is provided. Produces a human-friendly `## What's Changed` summary with optional GitHub alert blocks, a `Sources:` section preserving PR and commit references, and a full changelog compare link. | | [dotnet-change-impact](skills/dotnet-change-impact/SKILL.md) | Classify .NET library or NuGet package changes and recommend the correct release bump — `Major`, `Minor`, or `Patch` — for both Semantic Versioning (`MAJOR.MINOR.PATCH`) and .NET assembly/file versioning (`Major.Minor.Build.Revision`), grounded in Microsoft's official .NET compatibility rules. Uses the current Git branch by default when no explicit change details or compare range are provided, resolving it against the upstream/default base branch with local read-only git state. Always returns structured behavioral/binary/source/design-time/backwards compatibility reasoning with the recommendation, even when the bump is clear. | +| [dotnet-nuget-update](skills/dotnet-nuget-update/SKILL.md) | Audits and updates NuGet dependencies in .NET repositories with complete declaration accounting before any edit. It supports both `Directory.Packages.props` and project-level `PackageReference` versions, preserves XML structure and line endings, deduplicates package IDs, resolves independent live or offline flat-container version feeds with bounded parallel lookups and per-process memoization, uses bounded network timeouts, and merges results deterministically. Its single-process update runner keeps the audit, in-memory safe-update plan, and structural apply together for fast yolo passes. It keeps stable pins on stable candidates unless prerelease intent is explicit, and applies the TFM-band rule so conditional `net9`/`net10` package declarations stay within their matching major when that major is the compatibility signal rather than jumping to the newest overall release. Normal mode auto-applies revision/patch/minor and same-major prerelease updates, then batches majors for one approval decision; yolo mode applies only the auto classes and reports held majors without asking. | | [dotnet-docfx-digest](skills/dotnet-docfx-digest/SKILL.md) | Create and maintain developer-friendly DocFX documentation for .NET public APIs, including repo-wide no-input audits that inspect source, tests, DocFX config, DocFX `build.content` and `build.overwrite` Markdown inputs, namespace pages, and availability includes before asking for clarification, while treating bare direct skill invocations as autonomous repo-wide runs rather than human-driven checkpoint sessions. Enforces the workflow with two bundled .NET 10 file-based scripts resolved from the loaded skill directory, falling back to the repo-managed source path only when present: `scripts/agents.cs` writes an idempotent, marker-bounded DocFX maintenance block into the repository `AGENTS.md`; `scripts/docfx.cs` is **fast and build-free by default** — it validates Markdown, prose, DocFX overwrite layout, namespace overview pages, `Extension Members` tables, decorated receiver signatures such as `IDecorator`, generic method displays such as `As`, purpose-first summaries, and required per-type/extension examples without invoking `dotnet`, `msbuild`, `docfx`, or `gh`, discovering the public API from existing DocFX YAML metadata or a conservative source scan and ending every run with a `[processes] dotnet=0 msbuild=0 docfx=0 gh=0` summary plus per-phase timings. Compilation and network access are strictly opt-in: `--validate-samples` compiles each C# sample in an isolated project while batching all sample projects into one temporary `.slnx` graph build with bounded MSBuild parallelism and scoped references, `--build-api-model` (alias `--strict-api-discovery`) does reflection-backed discovery from compiled metadata via `MetadataLoadContext` through a single scoped `.slnx` graph build, `--verify-docfx-build` runs the DocFX CLI in a temp copy, and `--search-examples` runs `gh` code search. Final verification adapts to available processors and memory, overlaps isolated DocFX work on high-capacity machines, uses a 30-minute child timeout, and emits 10-second `stderr` heartbeats with active phase, workload, runner count, PID, elapsed time, last-output age, and current child output while preserving machine-readable JSON on `stdout`. Honors a single DocFX metadata `TargetFramework` when `--framework` is omitted, collapses C# 14 extension-block compiler containers such as `$...` back to the authored outer static class in both fast DocFX-YAML discovery and build-backed reflection discovery, validates namespace fly-ins that explain the problem solved/when to use/where to start plus example fly-ins before every C# fence, the Codebelt namespace-and-type-folder overwrite layout (`.docfx/api/namespaces/**/*.md` and `.docfx/api/types/**/*.md` under `build.overwrite` only), keeps `--changed-only` validation scoped to affected docs and APIs while still including brand-new untracked overwrite Markdown, uses the root Codebelt `.snk` when present and falls back to `-p:SkipSignAssembly=true` for keyless strong-name build verification, drains child stdout and stderr concurrently to avoid verbose-build deadlocks, writes deterministic `--assessment-queue` Markdown work queues for noisy audits, preserves working URL references unless a verified HTTP 404 justifies removal, treats unexpected new repo-root or DocFX-workspace files that are not known `dotnet-docfx-digest` deliverables as blocking cleanup diagnostics, keeps assessment/manifests/captured output/helper scripts in temp or session storage instead of the target repository, requires a namespace-first pass across the active queue before net-new type/example authoring during full audits, keeps deeper `EXTENSION_METHOD_MISSING` and `EXTENSION_METHOD_SIGNATURE_MISSING` follow-on diagnostics in that same namespace-layer table-repair phase when they appear after `EXTENSION_SECTION_MISSING` drops, preserves existing BOM and line-ending state while flagging actual mojibake instead of creating encoding-only diffs, and leaves generated DocFX YAML metadata untouched unless `--clean-generated-metadata` is explicitly requested (which runs only after the API model is built, never deleting metadata the run relied on). Documents public API only, uses bundled reference docs for overwrite rules, workflow details, and script behavior, keeps authored API overwrite Markdown under `.docfx/api/namespaces/` and `.docfx/api/types/`, moves legacy authored `.docfx/api/*.md` overwrite files there instead of widening the glob to `api/**/*.md`, teaches namespace and API prose to orient newcomers around purpose instead of inventorying contents, prefers inline or small sibling-batch prose repairs over slow per-page worker fan-out, makes examples start from package-ID usage evidence before type/member-only searches and requires each example to introduce the consumer task before the code, allows multi-type Microsoft Learn-style scenario samples when they better explain the consumer workflow, keeps extension-method examples on readable declaring-class type pages under `.docfx/api/types/` instead of synthetic method-UID filenames or namespace pages that mix extra `uid:` / `example:` blocks into the overview, flags weak skip-compile reasons, requires deterministic `.docfx/skip-compile-allowlist.json` entries for any pre-existing approved skip waivers, treats newly introduced or unallowlisted skip markers as fail-level diagnostics that do not suppress compilation, establishes reflection-backed packets with `--build-api-model --project-manifest` before full-run authoring, forces mid-audit continuations to name that manifest or the sequential assessment/namespace-first fallback explicitly, requires those continuations to restate the fast `docfx.cs --json` rerun cadence, the exact final `docfx.cs --build-api-model --validate-samples --verify-docfx-build --json` gate, and the clean JSON completion contract instead of generic “verify later” prose, treats batch size only as rerun cadence rather than permission to stop, runs a completion repair loop that treats every diagnostic as active work regardless of age or volume, treats newly surfaced follow-on diagnostics as the next repair queue instead of a stop point, reruns packet discovery with `--build-api-model --project-manifest` when fast source-scan packets are unnamed or zero-project, falls back to sequential namespace-first or assessment work queue order when packet discovery is still unusable, treats `EXAMPLE_MISSING`, `EXAMPLE_LEAD_MISSING`, `EXAMPLE_ADVANCED_LEAD_MISSING`, `FAMILY_ANCHOR_EXAMPLE_MISSING`, `SAMPLE_STRUCTURE_INVALID`, `FAIL_NEW_SKIP_MARKER_INTRODUCED`, `SAMPLE_SKIP_NOT_ALLOWLISTED`, and `INTERIM_ARTIFACT_IN_WORKTREE` queues as core work rather than checkpoints or quality backlog, drives large example and lead queues through a concrete fast-path micro-loop (next item or next 3-5 items → rerun → continue), suppresses progress-table/checkpoint output until the completion contract is clean or a real external blocker is reported, treats premature completion-shaped handoffs as execution-protocol failures while the queue is still dirty, reserves the final `--build-api-model --validate-samples --verify-docfx-build` verification for the real end of the queue, exposes `summary.fullVerificationRan`, `summary.canClaimCompletion`, `summary.remainingWorkItems`, `summary.remainingDiagnosticsByCode`, `summary.newlyIntroducedSkipMarkers`, and `summary.interimArtifacts` as machine-readable final gates, reruns the fast `docfx.cs --json` after edits until the queue is empty, then runs the build-backed verification before completion, preserves manual edits and authored Markdown during cleanup, skips recursive generated-output cleanup when a target directory contains documentation or source files, and returns deterministic exit codes plus `--json` reports (including process counts, phase timings, warning counts, and skip-marker accounting) so CI can gate on real failures instead of AI claims. | | [dotnet-test](skills/dotnet-test/SKILL.md) | Moves xUnit projects onto Codebelt's entrypoint-owned test hosts, replacing Microsoft's ASP.NET-only `WebApplicationFactory`—and the hand-rolled `HostBuilder` that console and worker tests reach for because Microsoft ships no equivalent—with one family of abstractions where the application's own entry point owns startup. Invocation is the request: it inspects and refactors immediately instead of opening with a menu or a questionnaire. It deterministically inspects project roles, target frameworks, xUnit generation, package ownership, inheritance, application entry points—including Bootstrapper `MinimalConsoleProgram`, `MinimalWorkerProgram`, and `MinimalWebProgram` hosts—and every selected `WebApplicationFactory` usage; classifies ordinary unit, ASP.NET Core functional, and console/worker functional tests; modernizes xUnit v2 projects to xUnit v3 plus Microsoft Testing Platform without moving package ownership or changing frameworks; and resolves current stable compatible packages through NuGet-backed isolated compatibility-project restores, including the selected combined package set. Focused web tests use `WebApplicationTestFactory` with an explicit entrypoint-owned `ManagedWebApplicationFixture`, directly or through a narrow `Test`-derived harness; shared web fixtures use `WebApplicationTest` with `ManagedWebApplicationFixture`; focused console/worker tests use `ApplicationTestFactory` with `ManagedApplicationFixture`; and shared non-web fixtures use `ApplicationTest` with `ManagedApplicationFixture`. Deprecated blocking fixtures are migration inputs only and are never emitted because they are scheduled for removal. Functional migrations fail closed unless the chosen Codebelt pattern and managed fixture are present, the legacy or blocking fixture is absent, and test code does not reconstruct the production composition root with its own `WebApplication`, `TestServer`, or `HostBuilder`. Migrations preserve entrypoint-owned startup, host configuration, lazy start, clients, services, configuration, sync/async disposal, isolation, and existing test names, while fresh bootstraps add source-grounded behavior tests. Non-web tests stay in-process and require a resolvable Generic Host; test-only scope reports the exact production adaptation instead of silently rewriting startup or launching a process. | | [dotnet-benchmark](skills/dotnet-benchmark/SKILL.md) | Discovers, prioritizes, and authors trustworthy BenchmarkDotNet experiments for a .NET type following codebelt conventions and using the `Codebelt.Extensions.BenchmarkDotNet.Console` runner. It inspects implementation code, call sites, tests, existing benchmarks, and available profiles instead of benchmarking every public member; ranks likely high-impact operations; selects representative typical, boundary, scaling, and adverse cases; and rejects external-I/O or service-level questions that need profiling, macrobenchmarks, or load tests. It creates fair current-versus-candidate comparisons only when observable work is equivalent, uses baseline-free single-operation characterization when no honest comparator exists, prevents unrelated construction/formatting/equality/hash ratios, requires exact per-case correctness oracles plus a semantic preflight for truthful workload labels, hard-gates interpretation on a complete valid BenchmarkDotNet summary, preserves workload invariants such as selectivity and hit/miss ratios as sizes scale, distinguishes deferred pipeline creation from terminal/materialization work, and performs Release build, discovery listing, and dry execution before any explicit full run. Explicit `yolo` mode auto-accepts routine repo-derived defaults and the proposed plan, then proceeds through build/list/dry validation without confirmation churn; only a separate explicit human instruction can start a full performance run. Its runner preflight recognizes the standard Slim/runtime setup and explains when `SkipBenchmarksWithReports = true` plus a matching `reports/tuning/` artifact deliberately filters a benchmark, preventing needless class renames, disassembly, or tool thrash; after the first valid full result it stops unless deeper diagnostics could change a real engineering decision. Harness setup remains adaptive: it detects `.slnx`/`.sln`, CPM, existing `tuning/` projects, and a reusable `tooling/` runner, onboards only missing pieces, resolves package versions dynamically, and keeps the benchmark class in the SUT namespace. | @@ -253,6 +254,11 @@ npx skills add https://github.com/codebeltnet/agentic --skill dotnet-docfx-diges ```bash npx skills add https://github.com/codebeltnet/agentic --skill dotnet-benchmark ``` +`dotnet-nuget-update` + +```bash +npx skills add https://github.com/codebeltnet/agentic --skill dotnet-nuget-update +``` `dotnet-test` ```bash @@ -343,6 +349,8 @@ Sometimes the history is already written and the only thing you need is the fina ### Why git-keep-a-changelog? +On repeated runs, the entity resolver checks each proposed bullet section against the full release baseline. A new unreleased capability permits only `Added`, so later fixes and refinements stay within its introduction instead of accumulating under `Changed` or `Fixed`. + Writing `CHANGELOG.md` well is harder than it looks. Raw commit subjects are too noisy, PR titles often miss migration context, and release notes get much better when the writer actually reads the commit bodies and understands the net diff. That is where **git-keep-a-changelog** fits: it turns the current branch into a curated Keep a Changelog entry and creates or updates the file directly for review. - **Keep a Changelog first** — writes `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, and `Security` sections in the expected style @@ -621,6 +629,20 @@ Picking the wrong version number is one of the easiest ways to break downstream - **Precedence-aware** — mixed releases take the highest required bump, - **Special-case savvy** — dependency updates, bug fixes, new overloads, interface and enum changes, analyzers/source generators, TFM/platform support, and performance changes each get the right default and the right escalation triggers. +### Why dotnet-nuget-update? + +Dependency updates look simple until a repository encodes compatibility in its package graph. A `Directory.Packages.props` file can pin the same package differently for `net9` and `net10`, hold a package back with an inline comment because a newer release dropped a target framework, or mix stable and prerelease intent on purpose. A shallow “latest package wins” pass breaks those repos quietly. + +**dotnet-nuget-update** makes the complete audit the first-class artifact. It enumerates every declaration before editing, keeps `current + auto + approval + unresolved == declared` as a hard invariant, resolves versions from a live or offline flat-container feed with per-process memoization, and preserves XML comments, spacing, encoding, and line endings when it writes changes back. + +- **Complete graph first** — every `` declaration, and explicit project-level `PackageReference` when CPM is absent, becomes an audit row before any update is applied, +- **TFM-band aware** — a package pinned under `net9` or `net10` stays inside that matching major when the pinned major itself is the compatibility signal, while mismatched majors remain free to move, +- **Stable/prerelease intent inference** — stable pins stay on stable candidates; prerelease pins may move within prerelease lines; same-major prerelease movement is auto, not approval, +- **History-first notes** — adjacent XML comments surface as `note` fields, and note-bearing auto updates are explicitly marked `READ THE NOTE before applying`, +- **Normal and yolo modes** — normal mode batches majors into one approval decision after the full audit, while yolo mode applies only the safe classes and reports held majors without asking, +- **Structural edits only** — version updates touch only the targeted attribute or element and leave comments, blank lines, unrelated files, and existing line endings intact, +- **Offline-testable** — the bundled scripts accept filesystem flat-container fixtures so audit, comparison, and update logic can be regression-tested without network dependency. + ### Why dotnet-docfx-digest? API documentation rots the moment code changes. A new public type ships without a namespace page, an extension method never makes it into the `Extension Members` table, a copy/paste example silently stops compiling, and "availability" drifts away from the real target frameworks. The usual fix — telling an agent to "remember to update the docs" — relies on AI memory, which is exactly the thing that fails on the next change. diff --git a/scripts/eval-runners/runner-common.ps1 b/scripts/eval-runners/runner-common.ps1 index 89b26be..be01bd6 100644 --- a/scripts/eval-runners/runner-common.ps1 +++ b/scripts/eval-runners/runner-common.ps1 @@ -269,20 +269,22 @@ function Expand-WindowsShortPath { # is the final component. Walk existing components so a short # parent such as ADMINI~1 is expanded before comparing a nested # runner path. + # Use filesystem APIs in this hot loop; PowerShell provider calls + # multiply across every arm's repeated integrity checks. $current = $root $remaining = $fullPath.Substring($root.Length) -split '[\\/]' for ($componentIndex = 0; $componentIndex -lt $remaining.Count; $componentIndex++) { $component = [string]$remaining[$componentIndex] if ([string]::IsNullOrWhiteSpace($component)) { continue } - $next = Join-Path -Path $current -ChildPath $component - if (Test-Path -LiteralPath $next -PathType Container) { + $next = [System.IO.Path]::Combine($current, $component) + if ([System.IO.Directory]::Exists($next)) { $current = ([System.IO.DirectoryInfo]::new($next)).FullName - } elseif (Test-Path -LiteralPath $next -PathType Leaf) { + } elseif ([System.IO.File]::Exists($next)) { $current = ([System.IO.FileInfo]::new($next)).FullName } else { - $current = Join-Path -Path $current -ChildPath $component + $current = $next if ($componentIndex + 1 -lt $remaining.Count) { - $current = Join-Path -Path $current -ChildPath ([string]::Join([System.IO.Path]::DirectorySeparatorChar, @($remaining[($componentIndex + 1)..($remaining.Count - 1)] | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) }))) + $current = [System.IO.Path]::Combine($current, [string]::Join([System.IO.Path]::DirectorySeparatorChar, @($remaining[($componentIndex + 1)..($remaining.Count - 1)] | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) }))) } break } diff --git a/scripts/eval-runners/tests/test-codex-paths.ps1 b/scripts/eval-runners/tests/test-codex-paths.ps1 index 0f31a1b..c869c16 100644 --- a/scripts/eval-runners/tests/test-codex-paths.ps1 +++ b/scripts/eval-runners/tests/test-codex-paths.ps1 @@ -24,6 +24,51 @@ if ((ConvertTo-CodexComparablePath $comparable) -cne $comparable) { throw 'Path if (-not (Test-CodexPathInsideComparableRoot $staged (Join-Path $staged 'FORMS.md'))) { throw 'Staged descendants must remain inside their root.' } if (Test-CodexPathInsideComparableRoot $staged ($staged + '-other/FORMS.md')) { throw 'A sibling prefix is not a descendant.' } +if ($IsWindows) { + Add-Type -Namespace AgenticPathTests -Name Native -MemberDefinition @' +[System.Runtime.InteropServices.DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode, SetLastError = true)] +public static extern uint GetShortPathName(string path, System.Text.StringBuilder buffer, uint size); +[System.Runtime.InteropServices.DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode, SetLastError = true)] +public static extern uint GetLongPathName(string path, System.Text.StringBuilder buffer, uint size); +'@ + # TEMP itself can contain a short user-profile component. Derive the + # expected long spelling with Win32, independently of the helper under test. + $buffer = [Text.StringBuilder]::new(32768) + $length = [AgenticPathTests.Native]::GetLongPathName([IO.Path]::GetTempPath(), $buffer, $buffer.Capacity) + if ($length -eq 0 -or $length -ge $buffer.Capacity) { throw 'Cannot resolve the temporary directory long path.' } + $tempRoot = $buffer.ToString().TrimEnd('\', '/') + $pathRoot = Join-Path $tempRoot ('agentic-paths-' + [guid]::NewGuid().ToString('N')) + try { + $directory = [IO.Directory]::CreateDirectory((Join-Path $pathRoot 'long directory [literal] æ')).FullName + $file = Join-Path $directory 'long filename [literal].json' + [IO.File]::WriteAllText($file, '{}') + $missing = Join-Path $directory 'missing parent/child/file.json' + foreach ($path in @($directory, $file, $missing, [IO.Path]::GetPathRoot($directory))) { + if ((Expand-WindowsShortPath $path) -cne [IO.Path]::GetFullPath($path)) { throw "Long path spelling changed: $path" } + } + if ((Expand-WindowsShortPath (Join-Path $directory '../long directory [literal] æ')) -cne $directory) { throw 'Relative components must normalize.' } + + $shortCases = 0 + foreach ($path in @($directory, $file, $env:ProgramFiles)) { + $buffer = [Text.StringBuilder]::new(32768) + $length = [AgenticPathTests.Native]::GetShortPathName($path, $buffer, $buffer.Capacity) + if ($length -eq 0 -or $length -ge $buffer.Capacity) { throw "Cannot query short path: $path" } + $short = $buffer.ToString() + if ($short -ieq $path) { continue } # Volumes may have 8.3 creation disabled. + if (-not (Test-ExactObservedPath -Expected $path -Observed $short)) { throw "Short path must match its long spelling: $short" } + if ([IO.Directory]::Exists($path)) { + $suffix = 'missing parent/child/file.json' + if (-not (Test-ExactObservedPath -Expected (Join-Path $path $suffix) -Observed (Join-Path $short $suffix))) { throw 'A missing tail must retain expansion of its existing short parent.' } + } + $shortCases++ + } + Write-Output "Windows path compatibility: PASS; $shortCases native short-path cases" + } finally { + if ([IO.Path]::GetDirectoryName([IO.Path]::GetFullPath($pathRoot)) -ne $tempRoot) { throw 'Path fixture cleanup must stay under the temporary directory.' } + if (Test-Path -LiteralPath $pathRoot) { Remove-Item -LiteralPath $pathRoot -Recurse -Force } + } +} + foreach ($access in @('command', 'file')) { $isolation = [ordered]@{ ambient_skill_paths_observed = @($ambient); failures = @() } $parameters = @{ NativeSkillIsolation = $isolation; AllowedStagedSkillRoot = $staged } diff --git a/scripts/test-validation-suites.ps1 b/scripts/test-validation-suites.ps1 index 5fb3470..6440a52 100644 --- a/scripts/test-validation-suites.ps1 +++ b/scripts/test-validation-suites.ps1 @@ -37,3 +37,40 @@ foreach ($script in @('scripts/validate-skill-templates.ps1', 'scripts/eval-runn } if ($workflow -notmatch '(?m)^ validate-skill-templates:' -or $workflow -notmatch 'needs: validate' -or $workflow -notmatch "VALIDATION_RESULT -ne 'success'") { throw 'The required aggregate check must reject failed, skipped, or cancelled suites.' } Write-Output 'CI suite coverage: PASS' + +# Exercise the production validation block against both supported content layouts. +& { + $tokens = $null + $parseErrors = $null + $validatorAst = [Management.Automation.Language.Parser]::ParseFile((Join-Path $repoRoot 'scripts/validate-skill-templates.ps1'), [ref]$tokens, [ref]$parseErrors) + foreach ($name in @('Assert-Contains', 'Assert-NotContains')) { + $definition = $validatorAst.Find({ param($node) $node -is [Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name }, $true) + . ([scriptblock]::Create($definition.Extent.Text)) + } + $command = $validatorAst.Find({ param($node) $node -is [Management.Automation.Language.CommandAst] -and $node.GetCommandName() -eq 'Add-ValidationResult' -and $node.Extent.Text.Contains("-Name 'Git visual commits skill enforces subject, identity, and grouping locks'") }, $true) + $action = @($command.CommandElements | Where-Object { $_ -is [Management.Automation.Language.ScriptBlockExpressionAst] })[0].ScriptBlock.GetScriptBlock() + $skillPath = 'skills/git-visual-commits/SKILL.md' + $examplesPath = 'skills/git-visual-commits/references/grouping-examples.md' + $currentSkill = [IO.File]::ReadAllText((Join-Path $repoRoot $skillPath)) + $examples = [IO.File]::ReadAllText((Join-Path $repoRoot $examplesPath)) + function Get-FileText { + param($RepoRoot, $RelativePath, $GitRef) + if ($RelativePath -eq $skillPath) { return $fixtureSkill } + if ($RelativePath -eq $examplesPath -and $missingExamples) { throw 'Missing grouping examples fixture' } + return [IO.File]::ReadAllText((Join-Path $RepoRoot $RelativePath)) + } + foreach ($case in @('current', 'historical-inline', 'historical-linked', 'broken-historical-link', 'missing-current-reference', 'missing-historical-content')) { + $Ref = if ($case -in @('current', 'missing-current-reference')) { '' } else { 'fixture-ref' } + $fixtureSkill = $currentSkill + $missingExamples = $case -in @('historical-inline', 'broken-historical-link', 'missing-current-reference', 'missing-historical-content') + if ($case -in @('historical-inline', 'missing-historical-content')) { + $fixtureSkill = $currentSkill.Replace('[grouping-examples.md](references/grouping-examples.md)', '') + if ($case -eq 'historical-inline') { $fixtureSkill += "`n$examples" } + } + $failure = $null + try { & $action } catch { $failure = $_ } + $shouldFail = $case -in @('broken-historical-link', 'missing-current-reference', 'missing-historical-content') + if ($shouldFail -ne ($null -ne $failure)) { throw "Grouping reference regression '$case' failed: $failure" } + } + Write-Output 'Grouping reference layouts: PASS (6 cases)' +} diff --git a/scripts/validate-skill-templates.ps1 b/scripts/validate-skill-templates.ps1 index 6858d9c..43e5935 100644 --- a/scripts/validate-skill-templates.ps1 +++ b/scripts/validate-skill-templates.ps1 @@ -1469,12 +1469,16 @@ Add-ValidationResult -Results $results -Name 'Skill evaluation prepares portable Assert-Contains -Name 'AGENTS.md' -Content $agents -Needle '### Asking for an eval' Assert-Contains -Name 'AGENTS.md' -Content $agents -Needle '`eval `, `evaluate `' Assert-Contains -Name 'AGENTS.md' -Content $agents -Needle 'Resolve the execution configuration before running the package preparation script.' - Assert-Contains -Name 'AGENTS.md' -Content $agents -Needle '### Eval preparation is a completion gate' - Assert-Contains -Name 'AGENTS.md' -Content $agents -Needle 'Adding or modifying any repo-managed skill triggers this workflow.' - Assert-Contains -Name 'AGENTS.md' -Content $agents -Needle 'pwsh -NoProfile -NonInteractive -File ./scripts/prepare-skill-evals.ps1 -Changed' - Assert-Contains -Name 'AGENTS.md' -Content $agents -Needle 'Preparing and reporting satisfies this gate. Executing a prompt never does' - Assert-Contains -Name 'AGENTS.md' -Content $agents -Needle '`scripts/sync-skill-install.ps1` runs last' - Assert-Contains -Name 'README.md' -Content $readme -Needle 'a completion gate an agent cannot skip' + Assert-Contains -Name 'AGENTS.md' -Content $agents -Needle '### Optional eval preparation' + Assert-Contains -Name 'AGENTS.md' -Content $agents -Needle 'Eval package preparation is optional.' + Assert-Contains -Name 'AGENTS.md' -Content $agents -Needle 'Prepare a package only when the user explicitly asks for an evaluation' + Assert-Contains -Name 'AGENTS.md' -Content $agents -Needle 'A request to create, modify, validate, or release a skill does not implicitly ask for eval preparation.' + Assert-Contains -Name 'AGENTS.md' -Content $agents -Needle 'package preparation is not a completion gate' + Assert-NotContains -Name 'AGENTS.md' -Content $agents -Needle 'Adding or modifying any repo-managed skill triggers this workflow.' + Assert-NotContains -Name 'AGENTS.md' -Content $agents -Needle '### Eval preparation is a completion gate' + Assert-NotContains -Name 'README.md' -Content $readme -Needle 'a completion gate an agent cannot skip' + Assert-Contains -Name 'README.md' -Content $readme -Needle 'Package preparation is optional and happens only when the user explicitly asks for an evaluation' + Assert-Contains -Name 'CONTRIBUTING.md' -Content $contributing -Needle 'Package preparation is optional and happens only after an explicit eval request' Assert-Contains -Name 'CONTRIBUTING.md' -Content $contributing -Needle 'pwsh -NoProfile -NonInteractive -File ./scripts/prepare-skill-evals.ps1 -Changed' Assert-Contains -Name 'README.md' -Content $readme -Needle 'prepares the paired candidate and baseline inputs as a portable package and stops' Assert-Contains -Name 'CONTRIBUTING.md' -Content $contributing -Needle 'pwsh -NoProfile -NonInteractive -File ./scripts/prepare-skill-evals.ps1 -Skill -Runner -Model ' @@ -2745,6 +2749,14 @@ Add-ValidationResult -Results $results -Name 'Git visual commits skill enforces $skill = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/git-visual-commits/SKILL.md' -GitRef $Ref $evals = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/git-visual-commits/evals/evals.json' -GitRef $Ref $commitLanguage = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/git-visual-commits/references/commit-language.md' -GitRef $Ref + $groupingExamplesName = 'git-visual-commits/SKILL.md' + $groupingExamples = $skill + # Historical trees kept these examples inline before extracting the reference. + if ([string]::IsNullOrWhiteSpace($Ref) -or $skill.Contains('references/grouping-examples.md')) { + $groupingExamplesName = 'git-visual-commits/references/grouping-examples.md' + $groupingExamples = Get-FileText -RepoRoot $repoRoot -RelativePath "skills/$groupingExamplesName" -GitRef $Ref + Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle '[grouping-examples.md](references/grouping-examples.md)' + } $subjectValidator = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/git-visual-commits/scripts/validate-commit-subject.ps1' -GitRef $Ref $subjectTests = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/git-visual-commits/scripts/test-commit-subject.ps1' -GitRef $Ref $readme = Get-FileText -RepoRoot $repoRoot -RelativePath 'README.md' -GitRef $Ref @@ -2811,17 +2823,17 @@ Add-ValidationResult -Results $results -Name 'Git visual commits skill enforces Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle '#### Single-category context quality gate' Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle 'Exactly one changed file is the only fast-path exception; skip this gate for that case.' Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle 'Did I actually read the whole `git-visual-commits` skill through EOF in this session before classifying this change?' - Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle 'Then re-check the complete `git status`, `git diff`, and applicable staged diff; enumerate every changed path' + Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle 'Then refresh the Step 1 inventory and review tracked diffs and untracked contents; explain each path''s rationale, audience, and lifecycle' Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle 'Put a visible line in the commit plan such as `Quality gate:' Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle '`yolo` and `auto` do not bypass this gate.' - Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle '#### Release-adjacent splitting rule' - Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle 'Concrete example: if one diff updates `Directory.Build.targets`, `Directory.Packages.props`, or `testenvironments.json`,' - Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle 'Keep `.nuget/*/PackageReleaseNotes.txt` with the' - Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle 'The rule is the abstraction: split by purpose and audience, not by the fact that the changes landed together.' + Assert-Contains -Name $groupingExamplesName -Content $groupingExamples -Needle '#### Release-adjacent splitting rule' + Assert-Contains -Name $groupingExamplesName -Content $groupingExamples -Needle 'Concrete example: if one diff updates `Directory.Build.targets`, `Directory.Packages.props`, or `testenvironments.json`,' + Assert-Contains -Name $groupingExamplesName -Content $groupingExamples -Needle 'Keep `.nuget/*/PackageReleaseNotes.txt` with the' + Assert-Contains -Name $groupingExamplesName -Content $groupingExamples -Needle 'The rule is the abstraction: split by purpose and audience, not by the fact that the changes landed together.' Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle 'do not collapse "new skill introduced" and "existing skill refactored" into one commit' - Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle '**New repo-managed skill**' - Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle 'a newly introduced `skills//` folder and its local `evals/` or `references/`' - Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle 'If a commit both introduces a brand-new skill and refactors an existing skill to support it, prefer separate commits.' + Assert-Contains -Name $groupingExamplesName -Content $groupingExamples -Needle '**New repo-managed skill**' + Assert-Contains -Name $groupingExamplesName -Content $groupingExamples -Needle 'a newly introduced `skills//` folder and its local `evals/` or `references/`' + Assert-Contains -Name $groupingExamplesName -Content $groupingExamples -Needle 'If a commit both introduces a brand-new skill and refactors an existing skill to support it, prefer separate commits.' Assert-Contains -Name 'git-visual-commits/references/commit-language.md' -Content $commitLanguage -Needle '### Allowed Prefixes' Assert-Contains -Name 'git-visual-commits/references/commit-language.md' -Content $commitLanguage -Needle '### Emoji Selection' Assert-Contains -Name 'git-visual-commits/references/commit-language.md' -Content $commitLanguage -Needle 'Gitmoji First, Fallback Second' @@ -2981,6 +2993,9 @@ Add-ValidationResult -Results $results -Name 'Git keep a changelog skill updates Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'run `scripts/resolve-release-entity.ps1` with the emitted `merge_base` and `head_commit`' Assert-Contains -Name 'git-keep-a-changelog/scripts/resolve-release-entity.ps1' -Content $entityResolver -Needle "'Added'" Assert-Contains -Name 'git-keep-a-changelog/scripts/resolve-release-entity.ps1' -Content $entityResolver -Needle "'Unchanged'" + Assert-Contains -Name 'git-keep-a-changelog/scripts/resolve-release-entity.ps1' -Content $entityResolver -Needle 'allowed_sections = @($allowedSections)' + Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle '-Section ' + Assert-Contains -Name 'git-keep-a-changelog/scripts/test-resolve-release-entity.ps1' -Content $entityResolverTests -Needle "Assert-Section -EntityPath 'skills/dotnet-test' -Section Fixed -Allowed `$false" Assert-Contains -Name 'git-keep-a-changelog/scripts/test-resolve-release-entity.ps1' -Content $entityResolverTests -Needle "Assert-Classification -EntityPath 'skills/dotnet-test' -Expected 'Added'" Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle '### Layered Capability Classification' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Do not use a top-level directory or the first framework commit as the only release entity.' diff --git a/skills/dotnet-nuget-update/SKILL.md b/skills/dotnet-nuget-update/SKILL.md new file mode 100644 index 0000000..877c8bf --- /dev/null +++ b/skills/dotnet-nuget-update/SKILL.md @@ -0,0 +1,194 @@ +--- +name: dotnet-nuget-update +description: > + Use when the user wants to update, audit, or check NuGet package dependencies + in a .NET repository. Handles both central package management + (Directory.Packages.props) and project-level PackageReference. Invoke as + "dotnet-nuget-update" for interactive normal mode (auto-applies patch/minor, + asks about majors) or "dotnet-nuget-update yolo" for silent patch/minor-only + mode. Use any time packages need updating, versions need auditing, or the + user asks about outdated dependencies. +--- + +# .NET NuGet Update + +Use this skill when a .NET repository needs a complete dependency audit or a controlled package update pass. + +## Start with the audit, not intuition + +Your first deterministic step is: + +```powershell +pwsh -NoProfile -File "/scripts/Get-DependencyAudit.ps1" -RepoRoot "" -MaxConcurrency 8 -TimeoutSec 15 +``` + +For yolo runs, keep the default bounded fan-out unless the feed or repository imposes a stricter limit. Do not resolve package IDs one at a time. All independent NuGet index requests should be issued through the audit's batch lookup; only declaration classification and file edits remain ordered. + +Use `scripts/Get-DependencyAudit.ps1 -RepoRoot ` to enumerate every declaration before touching anything. The skill is complete only when every declared package version is accounted for. + +The bundled scripts do the mechanical work: + +- `scripts/Get-DependencyAudit.ps1` enumerates and classifies each declaration. +- `scripts/Get-PackageGraph.ps1` exposes the central-package condition graph. +- `scripts/Get-TargetFrameworks.ps1` exposes the repository TFM matrix. +- `scripts/Resolve-NuGetVersion.ps1` and `scripts/Compare-Version.ps1` investigate one package or version pair. +- `scripts/Apply-PackageUpdates.ps1` performs the minimal structural edit. +- `scripts/Update-NuGetPackages.ps1` keeps the audit, in-memory safe-update plan, and structural apply in one orchestration command for yolo or attended auto-update runs. +- `scripts/Get-NuGetSources.ps1` shows configured package feeds without exposing secrets. + +The agent orchestrates. The scripts own the deterministic enumeration, comparison, and file edits. Dependency audits deduplicate package IDs and resolve independent NuGet version indexes with bounded parallel lookups (default concurrency: 8); repeated conditional declarations reuse the same response. Results are merged in declaration/source order, so concurrency changes elapsed time, not audit semantics. Network lookups use a bounded timeout and record failures as unresolved rows rather than retrying indefinitely. + +## Two modes + +### Normal mode + +Normal mode is for an attended update run. + +1. Audit the whole graph first, using the audit's deduplicated bounded-parallel feed lookup. +2. Auto-apply only `revision`, `patch`, `minor`, and same-major `prerelease` steps. +3. Do not interrupt the user for each package. +4. Batch all `major` candidates into one approval question after the full audit is complete. +5. Preserve any deliberately held pins and explain why they were held. + +### Yolo mode + +Yolo mode means no approval prompts, not broader authority. + +1. Audit the whole graph first, using the audit's deduplicated bounded-parallel feed lookup. +2. Auto-apply only `revision`, `patch`, `minor`, and same-major `prerelease` steps. +3. Hold all `major` candidates. +4. Report the held majors explicitly at the end. + +Yolo never means “apply majors silently,” and it never means commit or push anything. + +## The complete-audit invariant + +The audit is not optional scaffolding. It is the work list. + +For every run, ensure the summary closes: + +- `declared` +- `current` +- `auto` +- `approval` +- `unresolved` + +The invariant is: + +```text +current + auto + approval + unresolved == declared +``` + +Do not report the repository as updated unless every declaration is in exactly one bucket. A package that appears under two different conditions is two declarations and must produce two audit rows. + +## TFM-band rule + +Conditional central package graphs are load-bearing. Never flatten them. + +If a declaration lives under a modern .NET TFM condition and its pinned major matches that band, keep resolution inside that band. + +Examples: + +- `$(TargetFramework.StartsWith('net9'))` + `Microsoft.Extensions.Logging` `9.0.0` → resolve within `9.x` +- `$(TargetFramework.StartsWith('net10'))` + `Microsoft.EntityFrameworkCore` `10.0.0` → resolve within `10.x` +- `$(TargetFramework.StartsWith('net9'))` + `Asp.Versioning.Http` `8.1.0` → no band restriction, because package major `8` does not match band `9` +- `$(TargetFramework.StartsWith('net10')) OR $(TargetFramework.StartsWith('net11'))` + `10.0.0` → resolve within `10.x`, because `10` is one of the declared bands and matches the pinned major + +The band rule is a compatibility safeguard, not a guess about package policy. + +## Stable versus prerelease intent + +Infer package intent from the pin unless the user asks for something else. + +- If the pinned version is stable, prefer stable candidates only. +- If the pinned version is prerelease, allow prerelease candidates for that package. +- If the caller explicitly requests prerelease review, use `-IncludePrerelease`. +- Same-major prerelease movement is an `auto` class, not an approval class. + +That means `1.0.0-rc.1` → `1.0.0-rc.2` is a `prerelease` bump and may be auto-applied, while `13.0.3` → `14.0.0` remains `approval`. + +## History first: comments and past decisions + +Before changing a held or surprising pin, inspect its context. + +1. Read the adjacent XML comment through the audit `note` field. +2. Treat comments as maintainer intent, not decoration. +3. Review file or repository history when the pin looks deliberate or compatibility-sensitive. +4. If an `auto` candidate has a note, pause and read it before applying the update. + +A version comparison can tell you what is newer. It cannot tell you why a repository deliberately stayed behind. + +## Structural editing only + +NuGet props files and project files are structured XML, so edit them structurally and minimally. + +Use `scripts/Update-NuGetPackages.ps1 -RepoRoot -Yolo` for a yolo update pass. It applies only safe auto candidates without prompting, holds note-bearing auto candidates for review, holds majors, and reports unresolved rows. Use `-DryRun` to inspect the structural apply plan without writing. For manual or targeted edits, use `scripts/Apply-PackageUpdates.ps1` to update only the targeted declaration. Preserve: + +- conditions and item-group boundaries, +- comments and blank lines, +- package ordering, +- indentation, +- encoding, +- existing line endings. + +Do not rewrite the file wholesale. Change only the relevant `Version` attribute or project-level `` value. + +## Central package management and project-level references + +Handle both repository styles. + +### Central package management + +When `Directory.Packages.props` exists: + +- inspect it with `scripts/Get-PackageGraph.ps1`, +- audit it with `scripts/Get-DependencyAudit.ps1`, +- apply updates through `scripts/Apply-PackageUpdates.ps1`. + +### Project-level package references + +When the repository does not use central package management: + +- audit explicit project-level `PackageReference` versions, +- update only the affected `.csproj` files, +- preserve unrelated project content. + +Do not invent central package management for a repository that does not already use it. + +## Dirty working trees and conflicts + +Treat in-place dependency work as a surgical edit in a potentially dirty repository. + +- Do not overwrite a declaration whose current file value no longer matches the audited `from` version. +- If `scripts/Apply-PackageUpdates.ps1` reports `conflict`, stop and report it instead of guessing. +- Leave unrelated dirty files alone. +- Re-run the audit after applying updates when the repository state changed materially. + +A conflict is evidence that the file changed after the audit. Respect that evidence. + +## Validation + +Prefer the smallest deterministic validation that proves the update is safe. + +1. Discover target frameworks with `scripts/Get-TargetFrameworks.ps1 -RepoRoot `. + The static scanner resolves project properties through the nearest Directory.Build.props/targets and recursive explicit imports. It inventories declarations without evaluating MSBuild conditions or SDK imports; verify conditional results and unresolved tokens with project-specific MSBuild evaluation before selecting validation commands. +2. If source selection matters, inspect feeds with `scripts/Get-NuGetSources.ps1`. +3. Run the narrowest restore, build, or test command that covers the affected projects and target frameworks. +4. If the repository already has a targeted test or validation command, use it rather than inventing one. + +If no code changed because every declaration was already current, say so and report the audit summary anyway. + +## Reporting + +The final report must contain: + +1. Repository path and whether it used central or project-level package management. +2. Audit summary with `declared/current/auto/approval/unresolved` counts. +3. Every package actually updated, including condition or file context. +4. Every major candidate held for approval or yolo holdback. +5. Every unresolved package. +6. Every note-bearing declaration that was held or required human review. +7. Validation commands run and their outcomes. +8. Any conflicts or manual follow-up required. + +If nothing changed, say that explicitly and still include the complete audit summary. diff --git a/skills/dotnet-nuget-update/evals/evals.json b/skills/dotnet-nuget-update/evals/evals.json new file mode 100644 index 0000000..1a300b0 --- /dev/null +++ b/skills/dotnet-nuget-update/evals/evals.json @@ -0,0 +1,126 @@ +{ + "skill_name": "dotnet-nuget-update", + "evals": [ + { + "id": 8, + "prompt": "Inspect the target framework matrix in the attached multi-project repository before planning NuGet validation. Report each project's frameworks and any unresolved property references. Do not change package versions.", + "expected_output": "A resolves to net9.0, B resolves to net10.0, and C keeps $(PrivateFramework) unresolved because unused.props is not imported. Validation never borrows a sibling project's framework property.", + "expectations": [ + "Reports net9.0 for A and net10.0 for B", + "Reports C's PrivateFramework token as unresolved", + "Does not resolve C to net7.0 from the unimported props file" + ], + "files": [ + "evals/files/scoped-frameworks/a/Directory.Build.props", + "evals/files/scoped-frameworks/a/A.csproj", + "evals/files/scoped-frameworks/b/Directory.Build.props", + "evals/files/scoped-frameworks/b/B.csproj", + "evals/files/scoped-frameworks/c/C.csproj", + "evals/files/scoped-frameworks/unused.props" + ] + }, + { + "id": 1, + "prompt": "Audit the attached repository and update NuGet packages without breaking its TFM-specific package strategy. The repo targets net9.0 and net10.0, and the same Microsoft package is pinned separately for each band. Resolve every declaration first, then update each band to the newest version that still fits that band instead of proposing the overall newest major.", + "expected_output": "The agent performs a complete dependency audit before editing, updates Microsoft.Extensions.Logging separately within 9.x and 10.x, leaves Newtonsoft.Json as already current, and does not propose an 11.x jump for the band-tracked package.", + "expectations": [ + "Runs the complete dependency audit before changing any package", + "Treats the net9 declaration and the net10 declaration as separate rows", + "Updates the net9 package only within 9.x and the net10 package only within 10.x", + "Does not flatten the conditional graph into one shared package version", + "Reports the full audit summary with declared/current/auto/approval/unresolved counts" + ], + "files": [ + "evals/files/multi-tfm-bands/Directory.Packages.props", + "evals/files/multi-tfm-bands/Directory.Build.props" + ] + }, + { + "id": 2, + "prompt": "Update the attached central package graph. The latest overall Microsoft.Extensions.Logging release is a newer major, but the repository intentionally pins separate versions under net9 and net10 conditions. Finish the full audit and apply only the in-band updates that remain patch or minor steps.", + "expected_output": "The agent completes the audit without interruption, applies only in-band patch or minor updates, and reports any out-of-band latest major as held by the TFM-band rule instead of proposing it as the target.", + "expectations": [ + "Completes the audit before asking anything", + "Uses the TFM-band rule rather than the overall latest package version", + "Finishes without interruption when all in-band updates are auto classes", + "Explains when a newer overall version exists outside the allowed band" + ], + "files": [ + "evals/files/multi-tfm-bands/Directory.Packages.props", + "evals/files/multi-tfm-bands/Directory.Build.props" + ] + }, + { + "id": 3, + "prompt": "Review the attached mixed stable and prerelease package graph. Keep stable dependencies on stable releases, allow prerelease movement for the dependency that is already pinned to a prerelease, and avoid asking for approval when that prerelease update stays inside the same major.", + "expected_output": "Stable pins stay on stable candidates, the prerelease pin advances only within the intended prerelease line, and the same-major prerelease step is treated as an auto update instead of an approval candidate.", + "expectations": [ + "Infers stable intent from stable pins", + "Infers prerelease intent from the prerelease pin", + "Treats same-major prerelease movement as auto rather than approval", + "Still reports the complete audit summary" + ], + "files": [ + "evals/files/mixed-stable-prerelease/Directory.Packages.props", + "evals/files/mixed-stable-prerelease/Directory.Build.props" + ] + }, + { + "id": 4, + "prompt": "Use dotnet-nuget-update on the attached repository and apply all patch and minor NuGet updates. There are no majors that need approval, so the normal-mode run should complete without interrupting me.", + "expected_output": "The agent audits the repository, applies the safe updates, preserves the XML structure, and finishes without asking a question because only auto classes are present.", + "expectations": [ + "Normal mode does not interrupt the user when only auto updates are present", + "Applies patch and minor updates structurally rather than rewriting the file", + "Reports what changed and what remained current" + ], + "files": [ + "evals/files/flat-stable-only/Directory.Packages.props", + "evals/files/flat-stable-only/Directory.Build.props" + ] + }, + { + "id": 5, + "prompt": "Run dotnet-nuget-update in normal mode on the attached repository. Some dependencies have major upgrades available. Complete the full audit first, then ask only one batched approval question for the held majors instead of stopping package by package.", + "expected_output": "The agent finishes the full dependency audit before any approval prompt, batches the major candidates into one decision point, and separates those approval items from the auto-updatable packages.", + "expectations": [ + "Performs the complete audit before any approval question", + "Batches major candidates into one approval decision", + "Keeps auto candidates separate from approval candidates", + "Reports unresolved or note-bearing rows alongside the rest of the audit" + ], + "files": [ + "evals/files/with-xml-comment-pin/Directory.Packages.props", + "evals/files/with-xml-comment-pin/Directory.Build.props" + ] + }, + { + "id": 6, + "prompt": "Run dotnet-nuget-update yolo on the attached repository. Apply patch and minor updates silently, hold major upgrades without asking, and show the held majors in the final report.", + "expected_output": "The agent applies only the auto classes, leaves majors untouched, asks no follow-up question, and reports the held majors explicitly at the end.", + "expectations": [ + "Yolo mode never applies majors silently", + "Yolo mode asks no approval question", + "The final report lists held major candidates separately from applied updates" + ], + "files": [ + "evals/files/with-xml-comment-pin/Directory.Packages.props", + "evals/files/with-xml-comment-pin/Directory.Build.props" + ] + }, + { + "id": 7, + "prompt": "Update the attached repository, but pay attention to any inline notes explaining why a package is pinned. Preserve the comment, surface its warning in the audit, and avoid bulldozing through a deliberate hold just because the version step looks safe.", + "expected_output": "The agent preserves the XML comment, surfaces the note in the audit, and treats the note-bearing declaration as a deliberate human decision that must be read before applying any update.", + "expectations": [ + "Carries the adjacent XML comment into the audit note field", + "Preserves the comment and surrounding formatting during any edit", + "Marks a note-bearing auto candidate as needing the note to be read first" + ], + "files": [ + "evals/files/with-xml-comment-pin/Directory.Packages.props", + "evals/files/with-xml-comment-pin/Directory.Build.props" + ] + } + ] +} diff --git a/skills/dotnet-nuget-update/evals/files/flat-stable-only/Directory.Build.props b/skills/dotnet-nuget-update/evals/files/flat-stable-only/Directory.Build.props new file mode 100644 index 0000000..020f2b0 --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/flat-stable-only/Directory.Build.props @@ -0,0 +1,5 @@ + + + net10.0 + + diff --git a/skills/dotnet-nuget-update/evals/files/flat-stable-only/Directory.Packages.props b/skills/dotnet-nuget-update/evals/files/flat-stable-only/Directory.Packages.props new file mode 100644 index 0000000..8081d3b --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/flat-stable-only/Directory.Packages.props @@ -0,0 +1,9 @@ + + + true + + + + + + diff --git a/skills/dotnet-nuget-update/evals/files/mixed-stable-prerelease/Directory.Build.props b/skills/dotnet-nuget-update/evals/files/mixed-stable-prerelease/Directory.Build.props new file mode 100644 index 0000000..ccf46c5 --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/mixed-stable-prerelease/Directory.Build.props @@ -0,0 +1,5 @@ + + + net9.0;net10.0 + + diff --git a/skills/dotnet-nuget-update/evals/files/mixed-stable-prerelease/Directory.Packages.props b/skills/dotnet-nuget-update/evals/files/mixed-stable-prerelease/Directory.Packages.props new file mode 100644 index 0000000..6ae0ed0 --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/mixed-stable-prerelease/Directory.Packages.props @@ -0,0 +1,15 @@ + + + true + + + + + + + + + + + + diff --git a/skills/dotnet-nuget-update/evals/files/multi-tfm-bands/Directory.Build.props b/skills/dotnet-nuget-update/evals/files/multi-tfm-bands/Directory.Build.props new file mode 100644 index 0000000..fe5c3ec --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/multi-tfm-bands/Directory.Build.props @@ -0,0 +1,5 @@ + + + net9.0;net10.0 + + diff --git a/skills/dotnet-nuget-update/evals/files/multi-tfm-bands/Directory.Packages.props b/skills/dotnet-nuget-update/evals/files/multi-tfm-bands/Directory.Packages.props new file mode 100644 index 0000000..0c558b6 --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/multi-tfm-bands/Directory.Packages.props @@ -0,0 +1,13 @@ + + + true + + + + + + + + + + diff --git a/skills/dotnet-nuget-update/evals/files/plain-project-refs/Directory.Build.props b/skills/dotnet-nuget-update/evals/files/plain-project-refs/Directory.Build.props new file mode 100644 index 0000000..5f9708a --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/plain-project-refs/Directory.Build.props @@ -0,0 +1,5 @@ + + + false + + diff --git a/skills/dotnet-nuget-update/evals/files/plain-project-refs/src/App/App.csproj b/skills/dotnet-nuget-update/evals/files/plain-project-refs/src/App/App.csproj new file mode 100644 index 0000000..a5b106d --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/plain-project-refs/src/App/App.csproj @@ -0,0 +1,9 @@ + + + net10.0 + + + + + + diff --git a/skills/dotnet-nuget-update/evals/files/scoped-frameworks/a/A.csproj b/skills/dotnet-nuget-update/evals/files/scoped-frameworks/a/A.csproj new file mode 100644 index 0000000..36ff9df --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/scoped-frameworks/a/A.csproj @@ -0,0 +1 @@ +$(Framework) diff --git a/skills/dotnet-nuget-update/evals/files/scoped-frameworks/a/Directory.Build.props b/skills/dotnet-nuget-update/evals/files/scoped-frameworks/a/Directory.Build.props new file mode 100644 index 0000000..9018788 --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/scoped-frameworks/a/Directory.Build.props @@ -0,0 +1 @@ +net9.0 diff --git a/skills/dotnet-nuget-update/evals/files/scoped-frameworks/b/B.csproj b/skills/dotnet-nuget-update/evals/files/scoped-frameworks/b/B.csproj new file mode 100644 index 0000000..36ff9df --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/scoped-frameworks/b/B.csproj @@ -0,0 +1 @@ +$(Framework) diff --git a/skills/dotnet-nuget-update/evals/files/scoped-frameworks/b/Directory.Build.props b/skills/dotnet-nuget-update/evals/files/scoped-frameworks/b/Directory.Build.props new file mode 100644 index 0000000..d96e117 --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/scoped-frameworks/b/Directory.Build.props @@ -0,0 +1 @@ +net10.0 diff --git a/skills/dotnet-nuget-update/evals/files/scoped-frameworks/c/C.csproj b/skills/dotnet-nuget-update/evals/files/scoped-frameworks/c/C.csproj new file mode 100644 index 0000000..ec00223 --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/scoped-frameworks/c/C.csproj @@ -0,0 +1 @@ +$(PrivateFramework) diff --git a/skills/dotnet-nuget-update/evals/files/scoped-frameworks/unused.props b/skills/dotnet-nuget-update/evals/files/scoped-frameworks/unused.props new file mode 100644 index 0000000..ea4a503 --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/scoped-frameworks/unused.props @@ -0,0 +1 @@ +net7.0 diff --git a/skills/dotnet-nuget-update/evals/files/with-xml-comment-pin/Directory.Build.props b/skills/dotnet-nuget-update/evals/files/with-xml-comment-pin/Directory.Build.props new file mode 100644 index 0000000..3b7a0b3 --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/with-xml-comment-pin/Directory.Build.props @@ -0,0 +1,5 @@ + + + netstandard2.0 + + diff --git a/skills/dotnet-nuget-update/evals/files/with-xml-comment-pin/Directory.Packages.props b/skills/dotnet-nuget-update/evals/files/with-xml-comment-pin/Directory.Packages.props new file mode 100644 index 0000000..aed0669 --- /dev/null +++ b/skills/dotnet-nuget-update/evals/files/with-xml-comment-pin/Directory.Packages.props @@ -0,0 +1,11 @@ + + + true + + + + + + + + diff --git a/skills/dotnet-nuget-update/scripts/Apply-PackageUpdates.ps1 b/skills/dotnet-nuget-update/scripts/Apply-PackageUpdates.ps1 new file mode 100644 index 0000000..75c6355 --- /dev/null +++ b/skills/dotnet-nuget-update/scripts/Apply-PackageUpdates.ps1 @@ -0,0 +1,484 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory)][string]$RepoRoot, + [string]$Updates, + [string]$UpdatesFile, + [switch]$DryRun, + [switch]$AsJson +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. "$PSScriptRoot/_common.ps1" + +function Get-NodeVersion { + param([Parameter(Mandatory)][System.Xml.XmlNode]$Node) + + if ($Node.Attributes['Version']) { + return [string]$Node.Attributes['Version'].Value + } + if ($Node.Attributes['VersionOverride']) { + return [string]$Node.Attributes['VersionOverride'].Value + } + + $versionNode = $Node.SelectSingleNode('./Version') + if ($versionNode) { + return [string]$versionNode.InnerText + } + $overrideNode = $Node.SelectSingleNode('./VersionOverride') + if ($overrideNode) { + return [string]$overrideNode.InnerText + } + + return $null +} + +function Get-UpdateField { + param($Update, [Parameter(Mandatory)][string]$Name) + + if ($null -eq $Update) { return $null } + if ($Update -is [System.Collections.IDictionary]) { + if ($Update.Contains($Name)) { return $Update[$Name] } + return $null + } + $property = $Update.PSObject.Properties[$Name] + if ($null -ne $property) { return $property.Value } + return $null +} + +function Get-ConditionValue { + param($Node) + + if ($null -eq $Node) { return $null } + if ($Node.Attributes['Condition']) { return [string]$Node.Attributes['Condition'].Value } + return $null +} + +function Parse-Updates { + param([string]$Json, [string]$JsonFile) + + if (-not [string]::IsNullOrWhiteSpace($Json) -and -not [string]::IsNullOrWhiteSpace($JsonFile)) { + throw 'Specify either -Updates or -UpdatesFile, not both.' + } + + if (-not [string]::IsNullOrWhiteSpace($JsonFile)) { + $Json = Get-Content -Raw -LiteralPath $JsonFile + } + + if ([string]::IsNullOrWhiteSpace($Json)) { + throw 'One of -Updates or -UpdatesFile is required.' + } + + $parsed = $Json | ConvertFrom-Json + if ($parsed -is [string]) { return @($parsed) } + return @($parsed) +} + +function Replace-VersionInBlock { + param( + [Parameter(Mandatory)][string]$Block, + [Parameter(Mandatory)][string]$From, + [Parameter(Mandatory)][string]$To + ) + + $escaped = [regex]::Escape($From) + + foreach ($attributeName in @('Version', 'VersionOverride')) { + $pattern = '(' + $attributeName + '\s*=\s*)([''"])' + $escaped + '\2' + $match = [regex]::Match($Block, $pattern) + if ($match.Success) { + $prefix = $match.Groups[1].Value + $quote = $match.Groups[2].Value + $start = $match.Index + $prefix.Length + $quote.Length + return $Block.Substring(0, $start) + $To + $Block.Substring($start + $From.Length) + } + } + + foreach ($elementName in @('Version', 'VersionOverride')) { + $pattern = '(<' + $elementName + '\s*>\s*)' + $escaped + '(\s*)' + $match = [regex]::Match($Block, $pattern) + if ($match.Success) { + $prefix = $match.Groups[1].Value + $start = $match.Index + $prefix.Length + return $Block.Substring(0, $start) + $To + $Block.Substring($start + $From.Length) + } + } + + return $null +} + +function ConvertFrom-XmlText { + param([string]$Value) + + if ([string]::IsNullOrEmpty($Value)) { return $Value } + return [System.Net.WebUtility]::HtmlDecode($Value) +} + +function Update-DeclarationLine { + param( + [Parameter(Mandatory)][string]$Text, + [Parameter(Mandatory)][string]$ElementName, + [Parameter(Mandatory)][string]$Id, + [string]$Condition, + [Parameter(Mandatory)][string]$From, + [Parameter(Mandatory)][string]$To + ) + + $lines = [regex]::Split($Text, "`r`n|`n|`r") + $groupCondition = $null + + for ($index = 0; $index -lt $lines.Count; $index++) { + $line = $lines[$index] + + if ($line -match '' -and ($tagEnd + 1) -lt $lines.Count) { + $tagEnd++ + $tag += "`n" + $lines[$tagEnd] + } + $conditionMatch = [regex]::Match($tag, 'Condition\s*=\s*([''"])(.*?)\1') + $groupCondition = if ($conditionMatch.Success) { $conditionMatch.Groups[2].Value } else { $null } + if ($tagEnd -gt $index) { + $index = $tagEnd + $line = $lines[$index] + } + } + + if ($line -match '') { + $groupCondition = $null + } + + if ($line -notmatch ('<' + $ElementName + '\b')) { + continue + } + + $blockStart = $index + $tagText = $line + $tagEnd = $index + while ($tagText -notmatch '>' -and ($tagEnd + 1) -lt $lines.Count) { + $tagEnd++ + $tagText += "`n" + $lines[$tagEnd] + } + + $blockEnd = $tagEnd + $blockLines = @($lines[$blockStart..$blockEnd]) + if ($tagText -match '/\s*>\s*$' -or $tagText -match '/\s*>') { + $afterStart = $tagText.Substring($tagText.IndexOf('>') + 1) + if ($afterStart -match ('')) { + # Self-closing tag text already contains its close; block is complete. + } + } else { + $afterStart = '' + $greaterAt = $tagText.IndexOf('>') + if ($greaterAt -ge 0 -and $greaterAt + 1 -lt $tagText.Length) { + $afterStart = $tagText.Substring($greaterAt + 1) + } + if ($afterStart -notmatch ('')) { + $foundClose = $false + for ($closeIndex = $tagEnd + 1; $closeIndex -lt $lines.Count; $closeIndex++) { + $blockLines += $lines[$closeIndex] + $blockEnd = $closeIndex + if ($lines[$closeIndex] -match ('')) { + $foundClose = $true + break + } + if ($lines[$closeIndex] -match ' + + true + + + + + + + + +'@ + Write-File -Path (Join-Path $repoPath 'README.txt') -Content 'do not touch' + + $updates = '[{"id":"Newtonsoft.Json","condition":null,"from":"13.0.3","to":"13.0.4"},{"id":"Asp.Versioning.Http","condition":null,"from":"8.1.0","to":"8.1.1"}]' + $result = & pwsh -NoProfile -File (Join-Path $PSScriptRoot 'Apply-PackageUpdates.ps1') -RepoRoot $repoPath -Updates $updates -AsJson | ConvertFrom-Json + + Assert-Equal 'two central updates applied' (@($result.results | Where-Object { $_.outcome -eq 'applied' }).Count) 2 + + $updatedText = Get-Content -Raw -LiteralPath (Join-Path $repoPath 'Directory.Packages.props') + if ($updatedText -notmatch 'Newtonsoft\.Json" Version="13\.0\.4"' -or $updatedText -notmatch 'Asp\.Versioning\.Http" Version="8\.1\.1"') { + throw 'Updated package versions were not written back.' + } + if ($updatedText -notmatch '') { + throw 'XML comment was not preserved.' + } + Assert-Equal 'CRLF line endings are preserved' (Get-FileLineEndingStyle -Path (Join-Path $repoPath 'Directory.Packages.props')) 'CRLF' + Assert-Equal 'unrelated file stays untouched' (Get-Content -Raw -LiteralPath (Join-Path $repoPath 'README.txt')) 'do not touch' + + $conflict = & pwsh -NoProfile -File (Join-Path $PSScriptRoot 'Apply-PackageUpdates.ps1') -RepoRoot $repoPath -Updates '[{"id":"Newtonsoft.Json","condition":null,"from":"13.0.3","to":"13.0.5"}]' -AsJson | ConvertFrom-Json + Assert-Equal 'conflict is reported when from no longer matches' $conflict.results[0].outcome 'conflict' + + Write-Host 'test-apply-updates.ps1: PASS' +} +finally { + if (Test-Path -LiteralPath $workspace) { Remove-Item -LiteralPath $workspace -Recurse -Force -ErrorAction SilentlyContinue } +} diff --git a/skills/dotnet-nuget-update/scripts/test-dependency-audit.ps1 b/skills/dotnet-nuget-update/scripts/test-dependency-audit.ps1 new file mode 100644 index 0000000..d40cd0b --- /dev/null +++ b/skills/dotnet-nuget-update/scripts/test-dependency-audit.ps1 @@ -0,0 +1,87 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. "$PSScriptRoot/_common.ps1" + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path +$workspace = Join-Path $repoRoot ('.bot\dotnet-nuget-update-tests\audit-' + [Guid]::NewGuid().ToString('N')) +$utf8NoBom = [System.Text.UTF8Encoding]::new($false) + +function Write-File { + param([string]$Path, [string]$Content) + $directory = Split-Path -Parent $Path + if (-not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } + [System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom) +} + +function Assert-Equal { + param([string]$Name, $Actual, $Expected) + if ($Actual -ne $Expected) { throw "$Name failed. Expected '$Expected' but found '$Actual'." } +} + +New-Item -ItemType Directory -Path $workspace -Force | Out-Null +try { + $sourceRoot = Join-Path $workspace 'nuget-fixtures' + Write-File -Path (Join-Path $sourceRoot 'microsoft.extensions.logging\index.json') -Content '{ "versions": ["9.0.0","9.0.1","10.0.0","11.0.0-preview.1"] }' + Write-File -Path (Join-Path $sourceRoot 'newtonsoft.json\index.json') -Content '{ "versions": ["12.0.0","13.0.0","13.0.1","13.0.2","13.0.3"] }' + Write-File -Path (Join-Path $sourceRoot 'asp.versioning.http\index.json') -Content '{ "versions": ["8.0.0","8.1.0","8.1.1","9.0.0"] }' + Write-File -Path (Join-Path $sourceRoot 'somepackage\index.json') -Content '{ "versions": ["1.0.0","1.0.1"] }' + + Write-File -Path (Join-Path $workspace 'repo\Directory.Packages.props') -Content @' + + + true + + + + + + + + + + + + +'@ + Write-File -Path (Join-Path $workspace 'repo\Directory.Build.props') -Content @' + + + net9.0 + + +'@ + + $batch = @(Get-NuGetVersionListsBatch -Ids @('SomePackage', 'SomePackage', 'Newtonsoft.Json') -Sources @($sourceRoot) -MaxConcurrency 2 -TimeoutSec 2) + Assert-Equal 'batch lookup deduplicates package IDs' $batch.Count 2 + Assert-Equal 'batch lookup preserves first-seen package order' ($batch[0].id + ',' + $batch[1].id) 'SomePackage,Newtonsoft.Json' + + $audit = & pwsh -NoProfile -File (Join-Path $PSScriptRoot 'Get-DependencyAudit.ps1') -RepoRoot (Join-Path $workspace 'repo') -Source $sourceRoot -AsJson | ConvertFrom-Json + + $logging = $audit.rows | Where-Object { $_.id -eq 'Microsoft.Extensions.Logging' } + Assert-Equal 'net9 logging stays auto within band' $logging.action 'auto' + Assert-Equal 'net9 logging resolves to newest 9.x' $logging.candidate '9.0.1' + + $asp = $audit.rows | Where-Object { $_.id -eq 'Asp.Versioning.Http' } + Assert-Equal 'major candidate remains approval' $asp.action 'approval' + Assert-Equal 'major candidate resolves newest overall' $asp.candidate '9.0.0' + + $somePackage = $audit.rows | Where-Object { $_.id -eq 'SomePackage' } + Assert-Equal 'note field is populated' $somePackage.note 'Broken until downstream package catches up' + Assert-Equal 'note-bearing auto update stays auto' $somePackage.action 'auto' + if ($somePackage.reason -notmatch 'READ THE NOTE before applying') { + throw 'Expected note-bearing auto update reason to include READ THE NOTE before applying.' + } + + $unresolved = $audit.rows | Where-Object { $_.id -eq 'Unresolvable.Pkg' } + Assert-Equal 'unresolvable package becomes unresolved row' $unresolved.action 'unresolved' + Assert-Equal 'audit continues after unresolved package' $audit.summary.declared 5 + + $total = $audit.summary.current + $audit.summary.auto + $audit.summary.approval + $audit.summary.unresolved + Assert-Equal 'summary arithmetic closes' $total $audit.summary.declared + + Write-Host 'test-dependency-audit.ps1: PASS' +} +finally { + if (Test-Path -LiteralPath $workspace) { Remove-Item -LiteralPath $workspace -Recurse -Force -ErrorAction SilentlyContinue } +} diff --git a/skills/dotnet-nuget-update/scripts/test-package-graph.ps1 b/skills/dotnet-nuget-update/scripts/test-package-graph.ps1 new file mode 100644 index 0000000..2fbdff3 --- /dev/null +++ b/skills/dotnet-nuget-update/scripts/test-package-graph.ps1 @@ -0,0 +1,54 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path +$workspace = Join-Path $repoRoot ('.bot\dotnet-nuget-update-tests\graph-' + [Guid]::NewGuid().ToString('N')) +$utf8NoBom = [System.Text.UTF8Encoding]::new($false) + +function Write-File { + param([string]$Path, [string]$Content) + $directory = Split-Path -Parent $Path + if (-not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } + [System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom) +} + +function Assert-Equal { + param([string]$Name, $Actual, $Expected) + if ($Actual -ne $Expected) { throw "$Name failed. Expected '$Expected' but found '$Actual'." } +} + +New-Item -ItemType Directory -Path $workspace -Force | Out-Null +try { + Write-File -Path (Join-Path $workspace 'repo\Directory.Packages.props') -Content @' + + + true + + + + + + + + + + + +'@ + + $graph = & pwsh -NoProfile -File (Join-Path $PSScriptRoot 'Get-PackageGraph.ps1') -RepoRoot (Join-Path $workspace 'repo') -AsJson | ConvertFrom-Json + Assert-Equal 'graph reports file found' $graph.found $true + Assert-Equal 'graph counts all declarations' $graph.packageCount 4 + Assert-Equal 'same package under multiple conditions yields two rows' (@($graph.packages | Where-Object { $_.id -eq 'Microsoft.Extensions.Logging' }).Count) 2 + + $somePackage = $graph.packages | Where-Object { $_.id -eq 'SomePackage' } + Assert-Equal 'adjacent xml comment is preserved' $somePackage.note 'Pinned until downstream package catches up' + + $filtered = & pwsh -NoProfile -File (Join-Path $PSScriptRoot 'Get-PackageGraph.ps1') -RepoRoot (Join-Path $workspace 'repo') -Package 'Microsoft.Extensions.Logging' -AsJson | ConvertFrom-Json + Assert-Equal 'package filter keeps both conditional declarations' $filtered.packageCount 2 + + Write-Host 'test-package-graph.ps1: PASS' +} +finally { + if (Test-Path -LiteralPath $workspace) { Remove-Item -LiteralPath $workspace -Recurse -Force -ErrorAction SilentlyContinue } +} diff --git a/skills/dotnet-nuget-update/scripts/test-project-package-refs.ps1 b/skills/dotnet-nuget-update/scripts/test-project-package-refs.ps1 new file mode 100644 index 0000000..9c2c551 --- /dev/null +++ b/skills/dotnet-nuget-update/scripts/test-project-package-refs.ps1 @@ -0,0 +1,51 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path +$workspace = Join-Path $repoRoot ('.bot\dotnet-nuget-update-tests\project-' + [Guid]::NewGuid().ToString('N')) +$utf8NoBom = [System.Text.UTF8Encoding]::new($false) + +function Write-File { + param([string]$Path, [string]$Content) + $directory = Split-Path -Parent $Path + if (-not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } + [System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom) +} + +function Assert-Equal { + param([string]$Name, $Actual, $Expected) + if ($Actual -ne $Expected) { throw "$Name failed. Expected '$Expected' but found '$Actual'." } +} + +New-Item -ItemType Directory -Path $workspace -Force | Out-Null +try { + $repoPath = Join-Path $workspace 'repo' + Write-File -Path (Join-Path $repoPath 'src\App\App.csproj') -Content @' + + + net10.0 + + + + + + +'@ + Write-File -Path (Join-Path $repoPath 'Directory.Build.props') -Content '' + + $updates = '[{"id":"Newtonsoft.Json","condition":null,"from":"13.0.3","to":"13.0.4"},{"id":"Asp.Versioning.Http","condition":null,"from":"8.1.0","to":"8.1.1"}]' + $result = & pwsh -NoProfile -File (Join-Path $PSScriptRoot 'Apply-PackageUpdates.ps1') -RepoRoot $repoPath -Updates $updates -AsJson | ConvertFrom-Json + + Assert-Equal 'project-level mode is selected' $result.mode 'project' + Assert-Equal 'project-level updates applied' (@($result.results | Where-Object { $_.outcome -eq 'applied' }).Count) 2 + + $projectText = Get-Content -Raw -LiteralPath (Join-Path $repoPath 'src\App\App.csproj') + if ($projectText -notmatch 'Newtonsoft\.Json" Version="13\.0\.4"' -or $projectText -notmatch 'Asp\.Versioning\.Http" Version="8\.1\.1"') { + throw 'Project-level PackageReference versions were not updated.' + } + + Write-Host 'test-project-package-refs.ps1: PASS' +} +finally { + if (Test-Path -LiteralPath $workspace) { Remove-Item -LiteralPath $workspace -Recurse -Force -ErrorAction SilentlyContinue } +} diff --git a/skills/dotnet-nuget-update/scripts/test-target-frameworks.ps1 b/skills/dotnet-nuget-update/scripts/test-target-frameworks.ps1 new file mode 100644 index 0000000..25b047b --- /dev/null +++ b/skills/dotnet-nuget-update/scripts/test-target-frameworks.ps1 @@ -0,0 +1,40 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$workspace = Join-Path ([System.IO.Path]::GetTempPath()) ('dotnet-nuget-update-workspace/' + [Guid]::NewGuid().ToString('N')) +function Write-Fixture { + param([string]$Path, [string]$Content) + $destination = Join-Path $workspace $Path + New-Item -ItemType Directory -Path (Split-Path -Parent $destination) -Force | Out-Null + [System.IO.File]::WriteAllText($destination, $Content) +} +try { + Write-Fixture 'Directory.Build.props' 'net8.0' + Write-Fixture 'a/Directory.Build.props' 'net9.0' + Write-Fixture 'shared/first.props' '' + Write-Fixture 'shared/second.props' 'netstandard2.0' + Write-Fixture 'a/A.csproj' '$(Framework);$(ExtraFramework)' + Write-Fixture 'b/Directory.Build.props' 'net10.0' + Write-Fixture 'b/B.csproj' '$(Framework)' + Write-Fixture 'c/C.csproj' '$(PrivateFramework)' + Write-Fixture 'unused.props' 'net7.0' + Write-Fixture 'd/D.csproj' '$(Framework)' + Write-Fixture 'd/local.props' 'net6.0' + Write-Fixture 'e/E.csproj' 'net5.0$(Framework)' + Write-Fixture 'e/before.props' 'net6.0' + Write-Fixture 'e/after.targets' 'net7.0' + Write-Fixture 'e/Directory.Build.targets' 'net9.0' + $result = & "$PSScriptRoot/Get-TargetFrameworks.ps1" -RepoRoot $workspace + foreach ($case in @(@('a/A.csproj', 'net9.0,netstandard2.0'), @('b/B.csproj', 'net10.0'), @('c/C.csproj', ''), @('d/D.csproj', 'net6.0'), @('e/E.csproj', 'net5.0'))) { + $project = $result.projects | Where-Object { $_.path.Replace('\', '/') -eq $case[0] } + if (($project.targetFrameworks -join ',') -ne $case[1]) { throw "Wrong frameworks for $($case[0]): $($project.targetFrameworks -join ',')" } + } + if ($result.unresolvedTokens -notcontains '$(PrivateFramework)') { throw 'Unimported property must remain unresolved.' } + Write-Host 'test-target-frameworks.ps1: PASS' +} +finally { + $resolvedWorkspace = [System.IO.Path]::GetFullPath($workspace) + $allowedRoot = [System.IO.Path]::GetFullPath((Join-Path ([System.IO.Path]::GetTempPath()) 'dotnet-nuget-update-workspace')) + [System.IO.Path]::DirectorySeparatorChar + if (-not $resolvedWorkspace.StartsWith($allowedRoot, [System.StringComparison]::OrdinalIgnoreCase)) { throw 'Unsafe fixture cleanup path.' } + if (Test-Path -LiteralPath $resolvedWorkspace) { Remove-Item -LiteralPath $resolvedWorkspace -Recurse -Force } +} diff --git a/skills/dotnet-nuget-update/scripts/test-tfm-band.ps1 b/skills/dotnet-nuget-update/scripts/test-tfm-band.ps1 new file mode 100644 index 0000000..62592fa --- /dev/null +++ b/skills/dotnet-nuget-update/scripts/test-tfm-band.ps1 @@ -0,0 +1,79 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. "$PSScriptRoot/_common.ps1" + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path +$workspace = Join-Path $repoRoot ('.bot\dotnet-nuget-update-tests\tfm-' + [Guid]::NewGuid().ToString('N')) +$utf8NoBom = [System.Text.UTF8Encoding]::new($false) + +function Write-File { + param([string]$Path, [string]$Content) + $directory = Split-Path -Parent $Path + if (-not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } + [System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom) +} + +function Assert-Equal { + param([string]$Name, $Actual, $Expected) + if ($Actual -ne $Expected) { throw "$Name failed. Expected '$Expected' but found '$Actual'." } +} + +New-Item -ItemType Directory -Path $workspace -Force | Out-Null +try { + $sourceRoot = Join-Path $workspace 'nuget-fixtures' + Write-File -Path (Join-Path $sourceRoot 'microsoft.extensions.logging\index.json') -Content '{ "versions": ["9.0.0","9.0.5","10.0.0","10.0.2","11.0.0-preview.1"] }' + Write-File -Path (Join-Path $sourceRoot 'microsoft.entityframeworkcore\index.json') -Content '{ "versions": ["10.0.0","10.0.7","11.0.0-preview.1"] }' + Write-File -Path (Join-Path $sourceRoot 'asp.versioning.http\index.json') -Content '{ "versions": ["8.0.0","8.1.0","8.1.1","9.0.0"] }' + + Write-File -Path (Join-Path $workspace 'repo\Directory.Packages.props') -Content @' + + + true + + + + + + + + + + + + +'@ + Write-File -Path (Join-Path $workspace 'repo\Directory.Build.props') -Content @' + + + net9.0;net10.0;net11.0 + + +'@ + + Assert-Equal 'net9 band extraction' ((Get-TfmBand -Condition '$(TargetFramework.StartsWith(''net9''))') -join ',') '9' + Assert-Equal 'combined band extraction keeps both bands' ((Get-TfmBand -Condition '$(TargetFramework.StartsWith(''net10'')) OR $(TargetFramework.StartsWith(''net11''))') -join ',') '10,11' + + $audit = & pwsh -NoProfile -File (Join-Path $PSScriptRoot 'Get-DependencyAudit.ps1') -RepoRoot (Join-Path $workspace 'repo') -Source $sourceRoot -AsJson | ConvertFrom-Json + + $net9Logging = $audit.rows | Where-Object { $_.id -eq 'Microsoft.Extensions.Logging' -and $_.condition -eq '$(TargetFramework.StartsWith(''net9''))' } + Assert-Equal 'net9 package stays in 9.x band' $net9Logging.candidate '9.0.5' + Assert-Equal 'net9 package reports band 9' $net9Logging.band 9 + + $net10Logging = $audit.rows | Where-Object { $_.id -eq 'Microsoft.Extensions.Logging' -and $_.condition -eq '$(TargetFramework.StartsWith(''net10''))' } + Assert-Equal 'net10 package stays in 10.x band' $net10Logging.candidate '10.0.2' + Assert-Equal 'net10 package reports band 10' $net10Logging.band 10 + + $aspVersioning = $audit.rows | Where-Object { $_.id -eq 'Asp.Versioning.Http' } + Assert-Equal 'mismatched major infers no band' $aspVersioning.band $null + Assert-Equal 'mismatched major can select 9.0.0' $aspVersioning.candidate '9.0.0' + + $efCore = $audit.rows | Where-Object { $_.id -eq 'Microsoft.EntityFrameworkCore' } + Assert-Equal 'combined condition chooses band 10' $efCore.band 10 + Assert-Equal 'combined condition stays in 10.x' $efCore.candidate '10.0.7' + + Write-Host 'test-tfm-band.ps1: PASS' +} +finally { + if (Test-Path -LiteralPath $workspace) { Remove-Item -LiteralPath $workspace -Recurse -Force -ErrorAction SilentlyContinue } +} diff --git a/skills/dotnet-nuget-update/scripts/test-update-nuget-packages.ps1 b/skills/dotnet-nuget-update/scripts/test-update-nuget-packages.ps1 new file mode 100644 index 0000000..f964f42 --- /dev/null +++ b/skills/dotnet-nuget-update/scripts/test-update-nuget-packages.ps1 @@ -0,0 +1,63 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path +$workspace = Join-Path $repoRoot ('.bot\dotnet-nuget-update-tests\update-' + [Guid]::NewGuid().ToString('N')) +$utf8NoBom = [System.Text.UTF8Encoding]::new($false) + +function Write-File { + param([string]$Path, [string]$Content) + $directory = Split-Path -Parent $Path + if (-not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } + [System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom) +} + +function Assert-Equal { + param([string]$Name, $Actual, $Expected) + if ($Actual -ne $Expected) { throw "$Name failed. Expected '$Expected' but found '$Actual'." } +} + +New-Item -ItemType Directory -Path $workspace -Force | Out-Null +try { + $sourceRoot = Join-Path $workspace 'nuget-fixtures' + Write-File -Path (Join-Path $sourceRoot 'safe.package\index.json') -Content '{ "versions": ["1.0.0","1.0.1"] }' + Write-File -Path (Join-Path $sourceRoot 'note.package\index.json') -Content '{ "versions": ["1.0.0","1.0.1"] }' + Write-File -Path (Join-Path $sourceRoot 'major.package\index.json') -Content '{ "versions": ["1.0.0","2.0.0"] }' + + $repoPath = Join-Path $workspace 'repo' + Write-File -Path (Join-Path $repoPath 'Directory.Packages.props') -Content @' + + + true + + + + + + + + +'@ + + $runner = Join-Path $PSScriptRoot 'Update-NuGetPackages.ps1' + $result = & pwsh -NoProfile -File $runner -RepoRoot $repoPath -Yolo -Source $sourceRoot -AsJson | ConvertFrom-Json + Assert-Equal 'yolo mode is reported' $result.mode 'yolo' + Assert-Equal 'one safe update is applied' $result.appliedCount 1 + Assert-Equal 'safe update completes' $result.applyComplete $true + Assert-Equal 'major is held' @($result.heldMajors).Count 1 + Assert-Equal 'note-bearing auto candidate is held' @($result.noteHeld).Count 1 + + $updatedText = Get-Content -Raw -LiteralPath (Join-Path $repoPath 'Directory.Packages.props') + if ($updatedText -notmatch 'Safe\.Package" Version="1\.0\.1"') { throw 'Safe package was not updated.' } + if ($updatedText -notmatch 'Note\.Package" Version="1\.0\.0"') { throw 'Note-bearing package was changed unexpectedly.' } + if ($updatedText -notmatch 'Major\.Package" Version="1\.0\.0"') { throw 'Major package was changed unexpectedly.' } + + $dryRun = & pwsh -NoProfile -File $runner -RepoRoot $repoPath -Yolo -Source $sourceRoot -DryRun -AsJson | ConvertFrom-Json + Assert-Equal 'dry-run reports no write for current repository' $dryRun.appliedCount 0 + Assert-Equal 'dry-run remains complete' $dryRun.applyComplete $true + + Write-Host 'test-update-nuget-packages.ps1: PASS' +} +finally { + if (Test-Path -LiteralPath $workspace) { Remove-Item -LiteralPath $workspace -Recurse -Force -ErrorAction SilentlyContinue } +} diff --git a/skills/dotnet-nuget-update/scripts/test-version-comparison.ps1 b/skills/dotnet-nuget-update/scripts/test-version-comparison.ps1 new file mode 100644 index 0000000..e89b897 --- /dev/null +++ b/skills/dotnet-nuget-update/scripts/test-version-comparison.ps1 @@ -0,0 +1,60 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. "$PSScriptRoot/_common.ps1" + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path +$workspace = Join-Path $repoRoot ('.bot\dotnet-nuget-update-tests\version-' + [Guid]::NewGuid().ToString('N')) +$utf8NoBom = [System.Text.UTF8Encoding]::new($false) + +function Write-File { + param([string]$Path, [string]$Content) + $directory = Split-Path -Parent $Path + if (-not (Test-Path -LiteralPath $directory)) { New-Item -ItemType Directory -Path $directory -Force | Out-Null } + [System.IO.File]::WriteAllText($Path, $Content, $utf8NoBom) +} + +function Assert-Equal { + param([string]$Name, $Actual, $Expected) + if ($Actual -ne $Expected) { + throw "$Name failed. Expected '$Expected' but found '$Actual'." + } +} + +New-Item -ItemType Directory -Path $workspace -Force | Out-Null +try { + $sourceRoot = Join-Path $workspace 'nuget-fixtures' + Write-File -Path (Join-Path $sourceRoot 'newtonsoft.json\index.json') -Content '{ "versions": ["12.0.0","13.0.0","13.0.1","13.0.2","13.0.3","13.0.4","14.0.0"] }' + Write-File -Path (Join-Path $sourceRoot 'awssdk.core\index.json') -Content '{ "versions": ["3.7.0.0","3.7.0.1","3.7.100.0","4.0.0.0","4.0.100.6","4.0.100.8"] }' + Write-File -Path (Join-Path $sourceRoot 'xunit.v3\index.json') -Content '{ "versions": ["1.0.0","1.1.0","2.0.0-pre.1","2.0.0-pre.2"] }' + Write-File -Path (Join-Path $sourceRoot 'somepackage.prerelease\index.json') -Content '{ "versions": ["1.0.0-rc.1","1.0.0-rc.2","2.0.0-rc.1"] }' + + $resolveStable = & pwsh -NoProfile -File (Join-Path $PSScriptRoot 'Resolve-NuGetVersion.ps1') -Id 'xunit.v3' -Source $sourceRoot -AsJson | ConvertFrom-Json + Assert-Equal 'stable latest ignores prerelease' $resolveStable.latest '1.1.0' + Assert-Equal 'latestStable reports stable version' $resolveStable.latestStable '1.1.0' + Assert-Equal 'latestAny reports prerelease version' $resolveStable.latestAny '2.0.0-pre.2' + + $resolveAny = & pwsh -NoProfile -File (Join-Path $PSScriptRoot 'Resolve-NuGetVersion.ps1') -Id 'somepackage.prerelease' -Source $sourceRoot -IncludePrerelease -AsJson | ConvertFrom-Json + Assert-Equal 'prerelease source reports latest prerelease' $resolveAny.latest '2.0.0-rc.1' + + $sorted = Sort-NuGetVersion -Version @('13.0.4', '13.0.2', '13.0.3') + Assert-Equal 'Sort-NuGetVersion uses semantic ordering' ($sorted -join ',') '13.0.2,13.0.3,13.0.4' + + $prereleaseVsRelease = & pwsh -NoProfile -File (Join-Path $PSScriptRoot 'Compare-Version.ps1') -A '1.0.0-rc.2' -B '1.0.0' -AsJson | ConvertFrom-Json + Assert-Equal 'prerelease sorts below release' $prereleaseVsRelease.relation 'lt' + + $releaseVsPrerelease = & pwsh -NoProfile -File (Join-Path $PSScriptRoot 'Compare-Version.ps1') -A '1.0.0' -B '1.0.0-rc.2' -AsJson | ConvertFrom-Json + Assert-Equal 'release sorts above prerelease' $releaseVsPrerelease.relation 'gt' + + Assert-Equal 'no bump when versions equal' (Get-NuGetVersionBump -From '13.0.3' -To '13.0.3') 'none' + Assert-Equal 'patch bump classification' (Get-NuGetVersionBump -From '13.0.3' -To '13.0.4') 'patch' + Assert-Equal 'minor bump classification' (Get-NuGetVersionBump -From '13.0.0' -To '13.1.0') 'minor' + Assert-Equal 'major bump classification' (Get-NuGetVersionBump -From '13.0.3' -To '14.0.0') 'major' + Assert-Equal 'revision bump classification' (Get-NuGetVersionBump -From '4.0.100.6' -To '4.0.100.8') 'revision' + Assert-Equal 'prerelease bump classification' (Get-NuGetVersionBump -From '1.0.0-rc.1' -To '1.0.0-rc.2') 'prerelease' + + Write-Host 'test-version-comparison.ps1: PASS' +} +finally { + if (Test-Path -LiteralPath $workspace) { Remove-Item -LiteralPath $workspace -Recurse -Force -ErrorAction SilentlyContinue } +} diff --git a/skills/dotnet-nuget-update/scripts/validate-skill.ps1 b/skills/dotnet-nuget-update/scripts/validate-skill.ps1 new file mode 100644 index 0000000..47f6503 --- /dev/null +++ b/skills/dotnet-nuget-update/scripts/validate-skill.ps1 @@ -0,0 +1,89 @@ +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$skillRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +$skillName = Split-Path -Leaf $skillRoot + +$requiredFiles = @( + 'SKILL.md', + 'evals/evals.json', + 'scripts/_common.ps1', + 'scripts/Get-PackageGraph.ps1', + 'scripts/Get-DependencyAudit.ps1', + 'scripts/Get-TargetFrameworks.ps1', + 'scripts/Resolve-NuGetVersion.ps1', + 'scripts/Compare-Version.ps1', + 'scripts/Apply-PackageUpdates.ps1', + 'scripts/Update-NuGetPackages.ps1', + 'scripts/Get-NuGetSources.ps1', + 'scripts/run-tests.ps1', + 'scripts/validate-skill.ps1', + 'scripts/test-version-comparison.ps1', + 'scripts/test-tfm-band.ps1', + 'scripts/test-package-graph.ps1', + 'scripts/test-dependency-audit.ps1', + 'scripts/test-apply-updates.ps1', + 'scripts/test-project-package-refs.ps1', + 'scripts/test-update-nuget-packages.ps1' +) + +foreach ($relativePath in $requiredFiles) { + if (-not (Test-Path -LiteralPath (Join-Path $skillRoot $relativePath) -PathType Leaf)) { + throw "Missing required file: $relativePath" + } +} + +$skillText = [System.IO.File]::ReadAllText((Join-Path $skillRoot 'SKILL.md')) +$frontmatter = [regex]::Match($skillText, '(?s)^---\r?\n(.*?)\r?\n---\r?\n') +if (-not $frontmatter.Success) { + throw 'SKILL.md must start with YAML frontmatter.' +} + +$nameMatch = [regex]::Match($frontmatter.Groups[1].Value, '(?m)^name:\s*(.+)$') +if (-not $nameMatch.Success) { + throw 'SKILL.md frontmatter must contain a name field.' +} + +if ($nameMatch.Groups[1].Value.Trim() -ne $skillName) { + throw "SKILL.md frontmatter name must match folder name '$skillName'." +} + +$scriptReferences = [regex]::Matches($skillText, 'scripts/[A-Za-z0-9._-]+\.ps1') | + ForEach-Object { $_.Value.Replace('/', '\') } | + Sort-Object -Unique +foreach ($reference in $scriptReferences) { + if (-not (Test-Path -LiteralPath (Join-Path $skillRoot $reference) -PathType Leaf)) { + throw "SKILL.md references a missing script: $reference" + } +} + +$evals = Get-Content -Raw -LiteralPath (Join-Path $skillRoot 'evals/evals.json') | ConvertFrom-Json +if ($evals.skill_name -ne $skillName) { + throw "evals/evals.json skill_name must equal '$skillName'." +} + +foreach ($eval in @($evals.evals)) { + foreach ($relativePath in @($eval.files)) { + if (-not (Test-Path -LiteralPath (Join-Path $skillRoot $relativePath) -PathType Leaf)) { + throw "Missing eval fixture: $relativePath" + } + } +} + +$commonPath = Join-Path $skillRoot 'scripts/_common.ps1' +& pwsh -NoProfile -Command ". '$commonPath'" +if ($LASTEXITCODE -ne 0) { + throw "_common.ps1 failed to load with exit code $LASTEXITCODE." +} + +foreach ($testScript in Get-ChildItem -LiteralPath (Join-Path $skillRoot 'scripts') -Filter 'test-*.ps1' -File) { + $tokens = $null + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile($testScript.FullName, [ref]$tokens, [ref]$errors) | Out-Null + if ($errors.Count -gt 0) { + $message = $errors | Select-Object -First 1 | ForEach-Object { $_.Message } + throw "$($testScript.Name) has a syntax error: $message" + } +} + +Write-Host 'dotnet-nuget-update skill validation: PASS' diff --git a/skills/git-keep-a-changelog/SKILL.md b/skills/git-keep-a-changelog/SKILL.md index e580468..39c32da 100644 --- a/skills/git-keep-a-changelog/SKILL.md +++ b/skills/git-keep-a-changelog/SKILL.md @@ -36,6 +36,7 @@ Only after this skill has been selected by explicit changelog or release-note in - For branch-derived scope, exclude every commit already reachable from the comparison branch. A merge-base is a boundary, not a release commit. - Run `scripts/resolve-release-scope.ps1` for branch-derived scope and use its emitted ranges without widening them. - For every path-backed release entity, run `scripts/resolve-release-entity.ps1` with the emitted `merge_base` and `head_commit`; use its classification instead of inferring `Added`, `Removed`, or `Changed` from commit verbs. +- Before writing each path-backed outcome, rerun that resolver with `-Section ` and require success. Its `allowed_sections` bind the final bullet, including on subsequent runs. - Never change range inclusivity because the changelog target is a concrete version instead of `[Unreleased]`. - Include commits from every author/contributor in the selected scope. Do not filter to the current git user, current contributor, bot identity, configured author, or "my changes" unless the user explicitly asks for an author-filtered changelog. - If the current branch starts with a version hint such as `v0.3.0/`, use that to target a concrete release heading. @@ -351,6 +352,8 @@ Do not over-classify from dramatic wording in a commit subject. The surviving de Write the release highlight first, then the populated sections. +Read `references/section-validation.md` and validate every proposed path-backed outcome with `-Section ` before writing. Reuse the complete branch baseline on subsequent runs and consolidate each outcome once across all sections. + - Map only the surviving outcomes into `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, and `Security`. - Keep bullets curated and human-written. - Merge overlapping commits into one bullet when they describe the same real outcome. diff --git a/skills/git-keep-a-changelog/evals/evals.json b/skills/git-keep-a-changelog/evals/evals.json index b2cbd6b..ba6b1fd 100644 --- a/skills/git-keep-a-changelog/evals/evals.json +++ b/skills/git-keep-a-changelog/evals/evals.json @@ -231,14 +231,17 @@ }, { "id": 20, - "prompt": "Create a deterministic temp git repo outside the current repository under `$env:TEMP`, then use git-keep-a-changelog there twice. Start from a tagged base release containing `CHANGELOG.md`, `README.md`, and `scripts/validate-skills.ps1`, with no `skills/dotnet-test/` directory and no dotnet-test registration. On branch `v0.9.0/dotnet-test`, commit these steps in order: introduce `skills/dotnet-test/SKILL.md`; add its references, assets, scripts, and evals; register it in README.md and the shared validator. Run the skill once so `CHANGELOG.md` contains a draft `## [0.9.0]` section with an `Added` bullet for dotnet-test. Then make these more unreleased commits on the same branch: refine `resolve-test-package-versions.ps1` to validate packages as a combined set, add `test-resolve-test-package-versions.ps1`, update `validate-skill.ps1` to run that regression test, and revise the README.md dotnet-test capability description. Re-run the skill to update `CHANGELOG.md` and stop after the edit.", + "prompt": "Create a deterministic temp git repo outside the current repository under `$env:TEMP`, then use git-keep-a-changelog there twice. Start from a tagged base release containing `CHANGELOG.md`, `README.md`, and `scripts/validate-skills.ps1`, with no `skills/dotnet-test/` directory and no dotnet-test registration. On branch `v0.9.0/dotnet-test`, commit these steps in order: introduce `skills/dotnet-test/SKILL.md`; add its references, assets, scripts, and evals; register it in README.md and the shared validator. Run the skill once so `CHANGELOG.md` contains a draft `## [0.9.0]` section with an `Added` bullet for dotnet-test. Then make these more unreleased commits on the same branch: refine `resolve-test-package-versions.ps1` to validate packages as a combined set, add `test-resolve-test-package-versions.ps1`, update `validate-skill.ps1` to run that regression test, revise the README.md dotnet-test capability description; and repair scoped-property resolution, XML decoding, conflict routing, and encoding in the new resolver. Seed stale Changed and Fixed bullets that repeat these same repairs in the existing draft. Re-run the skill to update `CHANGELOG.md` and stop after the edit.", "expected_output": "The existing 0.9.0 draft section is regenerated from the branch's current base-to-HEAD result, keeping dotnet-test as one Added outcome in its final form instead of appending a Changed section for later pre-release refinements.", "expectations": [ "Treats the existing `## [0.9.0]` section as an unreleased draft because the matching `v0.9.0` tag does not exist yet", "Regenerates the 0.9.0 highlight and populated sections from the resolved git result instead of using the earlier draft text as a second baseline", "Keeps the combined-set package validation, resolver test, validator update, and README wording changes within the Added dotnet-test outcome because the capability is still base-absent", "Does not preserve the earlier draft bullet as a frozen baseline that forces later refinements into `Changed`", - "Does not create a Changed section or Changed bullet for unreleased refinements to the still-unreleased dotnet-test introduction" + "Does not create a Changed section or Changed bullet for unreleased refinements to the still-unreleased dotnet-test introduction", + "Validates each proposed path-backed bullet with resolve-release-entity.ps1 -Section using the full branch baseline on both invocations", + "Uses allowed_sections to keep fixes to the new unreleased capability under Added, including scoped-property, XML-decoding, conflict-routing, and encoding corrections", + "Consolidates any duplicate Changed and Fixed descriptions of the same outcome into the single Added capability summary" ] }, { diff --git a/skills/git-keep-a-changelog/references/section-validation.md b/skills/git-keep-a-changelog/references/section-validation.md new file mode 100644 index 0000000..e16a444 --- /dev/null +++ b/skills/git-keep-a-changelog/references/section-validation.md @@ -0,0 +1,12 @@ +# Validate proposed sections + +Validate each proposed path-backed outcome before writing it. Reuse the full release scope from Step 1 and the entity boundary from Step 4e, including its supporting files. Pass the intended section to the resolver: + +```powershell +pwsh -NoProfile -File /scripts/resolve-release-entity.ps1 -Repository . -BaseCommit -HeadCommit -EntityPath -Section +``` + +Use `-IncludeWorktree` for approved pending changes as in Step 4e. A nonzero exit blocks that bullet. Correct its section and consolidate the outcome, then validate again. Keep the same baseline on a subsequent invocation: the previous changelog edit, latest fix commit, and earlier invocation are draft checkpoints, not release boundaries. Re-resolve the complete branch scope even when the conversation already contains an earlier draft. + +For example, a new package-update capability followed by scoped-property, XML-decoding, conflict-routing, and encoding fixes still has only `Added` in `allowed_sections`. Summarize its final usable behavior in one `Added` bullet. A separate pre-existing capability is checked against its own entity path. Before writing, reconcile every bullet against these checked outcomes, with each distinct outcome appearing once across all sections. + diff --git a/skills/git-keep-a-changelog/scripts/resolve-release-entity.ps1 b/skills/git-keep-a-changelog/scripts/resolve-release-entity.ps1 index 6783a07..8cf77ee 100644 --- a/skills/git-keep-a-changelog/scripts/resolve-release-entity.ps1 +++ b/skills/git-keep-a-changelog/scripts/resolve-release-entity.ps1 @@ -13,7 +13,11 @@ param( [string] $EntityPath, [Parameter()] - [switch] $IncludeWorktree + [switch] $IncludeWorktree, + + [Parameter()] + [ValidateSet('Added', 'Changed', 'Deprecated', 'Removed', 'Fixed', 'Security')] + [string] $Section ) Set-StrictMode -Version Latest @@ -120,6 +124,17 @@ else { 'Unchanged' } +$allowedSections = @(switch ($classification) { + 'Added' { @('Added') } + 'Removed' { @('Removed') } + 'Changed' { @('Changed', 'Deprecated', 'Fixed', 'Security') } + default { @() } +}) + +if ($Section -and $Section -notin @($allowedSections)) { + throw "Entity '$normalizedPath' is $classification from the resolved base to the final state; section '$Section' is invalid. Allowed sections: $(@($allowedSections) -join ', '). Regenerate the outcome from the full release scope." +} + [pscustomobject]@{ entity_path = $normalizedPath base_commit = $resolvedBase @@ -128,4 +143,5 @@ else { base_exists = $baseExists final_exists = $finalExists classification = $classification + allowed_sections = @($allowedSections) } | ConvertTo-Json diff --git a/skills/git-keep-a-changelog/scripts/test-resolve-release-entity.ps1 b/skills/git-keep-a-changelog/scripts/test-resolve-release-entity.ps1 index b44dcd0..17483b0 100644 --- a/skills/git-keep-a-changelog/scripts/test-resolve-release-entity.ps1 +++ b/skills/git-keep-a-changelog/scripts/test-resolve-release-entity.ps1 @@ -74,6 +74,30 @@ function Assert-Classification { } } +function Assert-Section { + param( + [string] $EntityPath, + [string] $Section, + [bool] $Allowed, + [switch] $IncludeWorktree + ) + + $arguments = @('-NoProfile', '-NonInteractive', '-File', $resolver, + '-Repository', $testRoot, '-BaseCommit', $script:baseCommit, + '-EntityPath', $EntityPath, '-Section', $Section) + if ($IncludeWorktree) { $arguments += '-IncludeWorktree' } + $output = @(& pwsh @arguments 2>&1) + $exitCode = $LASTEXITCODE + if ($Allowed) { + if ($exitCode -ne 0) { throw "Expected $Section for ${EntityPath}: $output" } + $result = ($output -join [Environment]::NewLine) | ConvertFrom-Json + if ($Section -notin $result.allowed_sections) { throw "Missing allowed section $Section." } + } + elseif ($exitCode -eq 0 -or ($output -join ' ') -notmatch 'section .+ is invalid') { + throw "Expected section rejection for $EntityPath / $Section; exit ${exitCode}: $output" + } +} + try { New-Item -ItemType Directory -Path $testRoot | Out-Null Invoke-TestGit -Arguments @('init', '--initial-branch=main') | Out-Null @@ -98,11 +122,29 @@ try { Assert-Classification -EntityPath 'skills/legacy' -Expected 'Removed' Assert-Classification -EntityPath 'README.md' -Expected 'Unchanged' + # Simulate an earlier changelog invocation, then more commits on the same branch. + Assert-Section -EntityPath 'skills/dotnet-test' -Section Added -Allowed $true + Set-TestFile -RelativePath 'CHANGELOG.md' -Content "## [2.0.0]`n`n### Added`n`n- New dotnet-test capability." + Add-TestCommit -Message 'record unreleased draft' + Set-TestFile -RelativePath 'skills/dotnet-test/references/testing.md' -Content 'scoped properties, XML decoding, conflict routing, encoding fixes' + Add-TestCommit -Message 'fix and harden new capability after draft' + Assert-Section -EntityPath 'skills/dotnet-test' -Section Added -Allowed $true + Assert-Section -EntityPath 'skills/dotnet-test' -Section Changed -Allowed $false + Assert-Section -EntityPath 'skills/dotnet-test' -Section Fixed -Allowed $false + Assert-Section -EntityPath 'skills/existing' -Section Changed -Allowed $true + Assert-Section -EntityPath 'skills/existing' -Section Fixed -Allowed $true + Assert-Section -EntityPath 'skills/existing' -Section Added -Allowed $false + Assert-Section -EntityPath 'skills/legacy' -Section Removed -Allowed $true + Assert-Section -EntityPath 'README.md' -Section Changed -Allowed $false + Set-TestFile -RelativePath 'skills/pending/SKILL.md' -Content 'pending' Assert-Classification -EntityPath 'skills/pending' -Expected 'Unchanged' Assert-Classification -EntityPath 'skills/pending' -Expected 'Added' -IncludeWorktree + Assert-Section -EntityPath 'skills/pending' -Section Added -Allowed $true -IncludeWorktree + Assert-Section -EntityPath 'skills/pending' -Section Fixed -Allowed $false -IncludeWorktree Write-Output 'PASS: release entities are classified from deterministic base and final-state existence.' + Write-Output 'PASS: repeated draft runs reject Changed and Fixed for new unreleased capabilities while preserving existing-capability sections.' } finally { if (Test-Path -LiteralPath $testRoot) { diff --git a/skills/git-visual-commits/SKILL.md b/skills/git-visual-commits/SKILL.md index 0700436..2f01cf6 100644 --- a/skills/git-visual-commits/SKILL.md +++ b/skills/git-visual-commits/SKILL.md @@ -1,7 +1,7 @@ --- name: git-visual-commits -description: > - Use when the user asks to stage or commit changes, write or review a commit message, or says `git bot commit`, `git commit`, or `git our commit`. Treat commit wording as an automatic trigger for this skill, not as a casual hint. `yolo` and `auto` only modify an explicit commit request. +description: > + Use when the user asks to stage or commit changes, write or review a commit message, or says `git bot commit`, `git commit`, or `git our commit`. Treat commit wording as an automatic trigger for this skill, not as a casual hint. `yolo` and `auto` only modify an explicit commit request. --- # Git Visual Commits @@ -10,37 +10,37 @@ description: > This skill drives the entire git commit workflow — reviewing changes, grouping them logically, composing messages with the right emoji, and only adding a conventional prefix when the user explicitly asks for that combo. It supports three identity modes: bot-attributed (`git bot commit`), human-attributed (`git commit`), and collaborative (`git our commit`). -## Critical Rules - -### Invocation Routing Lock - -- An explicit `git bot commit`, `git commit`, or `git our commit` phrase is an authoritative request to use this skill. Do not substitute a changelog, release-note, squash-summary, or generic commit workflow. -- Interpret `Please do a git bot commit yolo` as `git bot commit` identity plus auto-approval for the full current worktree. `yolo` is not the commit message, and it does not request a changelog. -- That exact combination is also an instruction to complete the commit workflow in the current turn after the required checks pass. Treat the visible plan as status information, not as a request for another approval; do not end with a pending plan or ask whether to proceed. -- Equivalent word order and punctuation, such as `git bot commit, yolo` or `yolo — do a git bot commit`, preserve the same routing when both the explicit commit command and modifier are present. -- A competing skill may run only when the user also explicitly requests its distinct output, such as updating `CHANGELOG.md`, drafting release notes, or producing a squash summary. - -### Full-Skill Read and Subject Lock - -Before running any Git command or composing a subject, read this `SKILL.md` completely from the first line through EOF. A metadata preview, excerpt, cached recollection, or partial read is not sufficient. If a tool truncates the file, continue from the first unread line until EOF before proceeding. - -The first visible character after the emoji and its single separator space must be lowercase. This is a blocking requirement, not a style suggestion. Every proposed subject must have this exact default shape: - -```text - -``` - -After selecting the emoji from the bundled `references/commit-language.md`, run the bundled deterministic validator before showing the subject in a plan and again immediately before passing it to Git: - -```powershell -pwsh -NoProfile -File /scripts/validate-commit-subject.ps1 -Subject '' -``` - -Only when the user explicitly requested the conventional-prefix combo, add `-PrefixMode Required`. Resolve `` from this skill's installed directory, not from the current repository. The validator must exit successfully. If it fails, correct the subject and rerun it; never show, commit, or preserve the invalid subject. `yolo` and `auto` do not bypass the full-read or subject-validation locks. - -The validator enforces an emoji present in the bundled reference table, exactly one ASCII space after it, a lowercase first description character, the opt-in prefix contract, and the 70-character maximum. Semantic emoji selection still comes from reading the reference and inspecting the actual diff. - -### Identity Lock +## Critical Rules + +### Invocation Routing Lock + +- An explicit `git bot commit`, `git commit`, or `git our commit` phrase is an authoritative request to use this skill. Do not substitute a changelog, release-note, squash-summary, or generic commit workflow. +- Interpret `Please do a git bot commit yolo` as `git bot commit` identity plus auto-approval for the full current worktree. `yolo` is not the commit message, and it does not request a changelog. +- That exact combination is also an instruction to complete the commit workflow in the current turn after the required checks pass. Treat the visible plan as status information, not as a request for another approval; do not end with a pending plan or ask whether to proceed. +- Equivalent word order and punctuation, such as `git bot commit, yolo` or `yolo — do a git bot commit`, preserve the same routing when both the explicit commit command and modifier are present. +- A competing skill may run only when the user also explicitly requests its distinct output, such as updating `CHANGELOG.md`, drafting release notes, or producing a squash summary. + +### Full-Skill Read and Subject Lock + +Before running any Git command or composing a subject, read this `SKILL.md` completely from the first line through EOF. A metadata preview, excerpt, cached recollection, or partial read is not sufficient. If a tool truncates the file, continue from the first unread line until EOF before proceeding. + +The first visible character after the emoji and its single separator space must be lowercase. This is a blocking requirement, not a style suggestion. Every proposed subject must have this exact default shape: + +```text + +``` + +After selecting the emoji from the bundled `references/commit-language.md`, run the bundled deterministic validator before showing the subject in a plan and again immediately before passing it to Git: + +```powershell +pwsh -NoProfile -File /scripts/validate-commit-subject.ps1 -Subject '' +``` + +Only when the user explicitly requested the conventional-prefix combo, add `-PrefixMode Required`. Resolve `` from this skill's installed directory, not from the current repository. The validator must exit successfully. If it fails, correct the subject and rerun it; never show, commit, or preserve the invalid subject. `yolo` and `auto` do not bypass the full-read or subject-validation locks. + +The validator enforces an emoji present in the bundled reference table, exactly one ASCII space after it, a lowercase first description character, the opt-in prefix contract, and the 70-character maximum. Semantic emoji selection still comes from reading the reference and inspecting the actual diff. + +### Identity Lock - If the user asked for `git bot commit`, you must use `git bot commit`. - If the user asked for `git commit`, you must use `git commit`. @@ -64,16 +64,16 @@ The validator enforces an emoji present in the bundled reference table, exactly ### Auto-Approval Guard -`yolo` / `auto` skips user confirmation only. It never skips: - -- skill activation -- the full-skill read -- identity selection -- semantic grouping -- mixed-scope validation -- deterministic subject validation -- post-commit author verification -- In auto-approval mode, the user's `yolo` or `auto` is already the approval for this commit request. After the required checks pass, execute the commit command(s) in the same turn. Do not ask "Proceed?", "Should I commit?", or any equivalent confirmation question, and do not return a plan as if approval were still pending. Stop only for a concrete blocker such as a missing alias or failed validation, and report that blocker directly. +`yolo` / `auto` skips user confirmation only. It never skips: + +- skill activation +- the full-skill read +- identity selection +- semantic grouping +- mixed-scope validation +- deterministic subject validation +- post-commit author verification +- In auto-approval mode, the user's `yolo` or `auto` is already the approval for this commit request. After the required checks pass, execute the commit command(s) in the same turn. Do not ask "Proceed?", "Should I commit?", or any equivalent confirmation question, and do not return a plan as if approval were still pending. Stop only for a concrete blocker such as a missing alias or failed validation, and report that blocker directly. If the user did **not** say `yolo` or `auto`, and session-level auto mode is not already enabled, do **not** run any commit command yet. You must stop after Step 4, present the plan, and wait for approval. @@ -81,7 +81,7 @@ If the user did **not** say `yolo` or `auto`, and session-level auto mode is not If the user says `git bot commit`, `git commit`, or `git our commit` without narrowing language, treat the request as covering the full current worktree. -- The default scope is **all current changes visible in git status**. +- The default scope is **all current changes visible in git status**: staged, unstaged, deleted, renamed, and non-ignored untracked files, including files inside new directories. - Your job is to group that full worktree into the right number of commits by semantic intent. - Never silently narrow the scope to "just the files from the last thing I worked on", "just the files I touched", or "just the newest skill" unless the user explicitly said to do that. - `yolo` keeps this same full-worktree default. It removes the approval wait; it does not narrow scope. @@ -94,6 +94,8 @@ Narrow scope only when the user explicitly does one of these: If the user did not narrow scope, do not invent a narrower scope on their behalf. +Use the expanded status inventory in Step 1 as the scope of record. `git diff`, `git diff --stat`, and `git diff --name-only` omit untracked files; none can replace that inventory. Untracked status alone is neither an exclusion nor a reason to ask permission. Respect Git's ignore rules for untracked files; do not force-add ignored files. Already tracked files remain in scope even if an ignore pattern now matches them. + ### Recovery Safety Rule - Before any destructive recovery command, inspect the current git state again with commands such as `git status`, `git diff`, `git diff --staged`, and when relevant `git reflog`. @@ -126,13 +128,13 @@ After every commit, run: git log -1 --format="%an <%ae>" ``` -Confirm that the author matches the requested identity mode. If the author is wrong, treat the commit as invalid and repair it before reporting success. - -```bash -git log -1 --format=%B -``` - -If the body contains literal escape sequences such as `\n` instead of real line breaks, treat the commit message as invalid and repair it before reporting success. +Confirm that the author matches the requested identity mode. If the author is wrong, treat the commit as invalid and repair it before reporting success. + +```bash +git log -1 --format=%B +``` + +If the body contains literal escape sequences such as `\n` instead of real line breaks, treat the commit message as invalid and repair it before reporting success. ### Umbrella Commit Rejection @@ -155,7 +157,7 @@ git config --global alias.bot '!git -c user.name="" -c user.email="` and `` with the identity you want AI-authored commits to appear under. -If `git config --global --get alias.bot` returns nothing when the user asked for `git bot commit`, stop and report that the bot alias is missing. Do not proceed with `git commit` as a fallback. +If `git config --global --get alias.bot` returns nothing when the user asked for `git bot commit`, stop and report that the bot alias is missing. Do not proceed with `git commit` as a fallback. --- @@ -169,9 +171,9 @@ In all cases, **the AI does all the work** — reviewing changes, grouping them | **Who gets credit** | Bot alias (e.g. `aicia[bot]`) | Human's default git profile | Agent analyzes authorship, human picks attribution | | **Command** | `git bot commit -m "..."` | `git commit -m "..."` | Either, based on human's choice | -### How `git our commit` works - -When the user says "our commit", analyze which changed files were agent-authored, human-authored, or mixed/unclear. Group by semantic intent first, then assign each all-agent group to `git bot commit` and each all-human group to `git commit`. For mixed/unclear groups, ask who should be the author. Present the attribution beside every planned commit; the user may override it. Do not add `Co-authored-by` trailers because the pull-request flow already records collaboration. +### How `git our commit` works + +When the user says "our commit", analyze which changed files were agent-authored, human-authored, or mixed/unclear. Group by semantic intent first, then assign each all-agent group to `git bot commit` and each all-human group to `git commit`. For mixed/unclear groups, ask who should be the author. Present the attribution beside every planned commit; the user may override it. Do not add `Co-authored-by` trailers because the pull-request flow already records collaboration. The commit message format, emoji conventions, grouping strategy, and everything else is **identical** for both. The profile is the only thing that changes. @@ -199,7 +201,7 @@ Only when the user explicitly asks for an emoji plus conventional-commit combo: - **Emoji** comes first — picked from `references/commit-language.md` - **Prefix** is omitted by default. Only add one when the user explicitly asked for an emoji plus conventional-commit combo. When combo mode is active, the prefix is lowercase (see `references/commit-language.md`) — **never use `feat:`** -- **Description** begins with a lowercase letter, uses imperative wording, and keeps the full subject to at most 70 characters (including emoji and any explicit-request prefix) +- **Description** begins with a lowercase letter, uses imperative wording, and keeps the full subject to at most 70 characters (including emoji and any explicit-request prefix) - **Body** is included by default — a short paragraph explaining *why* the change was made, not just *what* changed. Separate from the subject with a blank line. Do **not** hard-wrap commit bodies at 72 characters; keep short bodies as normal prose and add line breaks only when they improve readability. Can be suppressed with `no-body` (see below). - **Body repair rule** — if verification shows the stored body was split mid-sentence just to fit an arbitrary width, amend the commit before reporting success. - One logical change per commit — don't bundle unrelated things @@ -212,25 +214,25 @@ Treat `references/commit-language.md` as a bundled skill resource path, not as a That reference now defines prefixes as opt-in. Unless the user explicitly asked for an emoji plus conventional-commit combo, keep subjects in the default ` ` form. For community health, changelog, and release-status communication, prefer `💬` from that same reference rather than generic docs emoji. -### Source Discipline for Explanations - -Anchor emoji, prefix, and grouping explanations to sources inspected in the current session. Distinguish verified sources from inference, and never claim that a document, attachment, screenshot, or image contained guidance unless you verified it. +### Source Discipline for Explanations + +Anchor emoji, prefix, and grouping explanations to sources inspected in the current session. Distinguish verified sources from inference, and never claim that a document, attachment, screenshot, or image contained guidance unless you verified it. --- -## Auto-Approval Mode - -`yolo` or `auto` inside an explicit commit request skips the Step 4 approval wait for that request. `enable yolo mode` or `enable auto mode` keeps it active until the user disables it. Auto-approval applies to all identity modes and skips confirmation only; every classification, grouping, subject-validation, identity, and post-commit check still runs. Show the plan before proceeding: - -``` -Auto-committing: 🔧 build/toolchain → 🚚 moved types → 💥 breaking shim removal → 💬 release notes -``` - ---- - -## No-Body Mode - -Commits include a body by default. `no-body` or `tmi` suppresses it for one request; `enable no-body mode` or `enable tmi mode` keeps subjects-only until disabled. This mode suppresses only the body. Subject, emoji, prefix, classification, grouping, and validation rules still apply. +## Auto-Approval Mode + +`yolo` or `auto` inside an explicit commit request skips the Step 4 approval wait for that request. `enable yolo mode` or `enable auto mode` keeps it active until the user disables it. Auto-approval applies to all identity modes and skips confirmation only; every classification, grouping, subject-validation, identity, and post-commit check still runs. Show the plan before proceeding: + +``` +Auto-committing: 🔧 build/toolchain → 🚚 moved types → 💥 breaking shim removal → 💬 release notes +``` + +--- + +## No-Body Mode + +Commits include a body by default. `no-body` or `tmi` suppresses it for one request; `enable no-body mode` or `enable tmi mode` keeps subjects-only until disabled. This mode suppresses only the body. Subject, emoji, prefix, classification, grouping, and validation rules still apply. --- @@ -238,7 +240,9 @@ Commits include a body by default. `no-body` or `tmi` suppresses it for one requ ### Step 1: Review changes -Run `git status` and `git diff` (and `git diff --staged` if there are staged changes) to understand what has changed. +From the repository root, run `git status --short --untracked-files=all` to enumerate individual paths even when Git configuration hides untracked files. Keep this inventory through final verification. For programmatic parsing, use `git status --porcelain=v1 --untracked-files=all -z` and parse NUL-delimited records, including rename pairs; do not split filenames on whitespace. + +Review `git diff` and `git diff --staged` for tracked changes and inspect every untracked file directly with an appropriate text or binary viewer. An empty diff does not mean a clean worktree. Do not stage files just to make them appear in a diff. Unless the user explicitly narrowed scope, inspect the **entire current worktree** and build the commit plan from that full set of changes. Do not default to the last task only. @@ -250,23 +254,23 @@ When resolving that reference, prefer the bundled skill path first instead of tr ### Step 2: Classify changes -Before composing any commit message, bucket every changed file by its **semantic intent** — not just its file type. Read the actual diff for each file and ask: *"What is this change trying to accomplish?"* Two files of the same type (e.g. two test files) may have completely different intents and belong in separate commits. +Before composing any commit message, bucket every path in the Step 1 inventory by its **semantic intent** — not its tracking status or file type. Read the actual diff for tracked files and contents of untracked files and ask: *"What is this change trying to accomplish?"* Two files of the same type (e.g. two test files) may have completely different intents and belong in separate commits. Use the inspected commit-language reference as the meaning source, not your gut. For example, restructuring an existing skill's `SKILL.md`, `FORMS.md`, `references/`, or `evals/` is normally refactor intent and should map to `♻️`; configuration-file changes map to `🔧`; truly new repo or application capabilities map to `✨`. -#### Emoji Resolution: Common Mistakes - -Sparkles (`✨`) is only for a capability that did not exist before. Do not use it for fixes, documentation, improvements to existing behavior, refactors, or tests. When two emoji seem plausible, use the reference meaning that most closely describes what the diff actually does. +#### Emoji Resolution: Common Mistakes + +Sparkles (`✨`) is only for a capability that did not exist before. Do not use it for fixes, documentation, improvements to existing behavior, refactors, or tests. When two emoji seem plausible, use the reference meaning that most closely describes what the diff actually does. Derive categories from the actual diff — don't assume a fixed set. Common categories include: - **New repo capabilities** — introducing a new repo-managed skill, workflow, or top-level capability - **Existing skill refactors** — restructuring or extracting shared rules from an already existing skill - **Dependency/version baselines** — shared dependency manifests, package version props, runner-image version pins, or environment baselines that primarily align versions -- **Package/publish metadata** — release-note definitions, pack/publish targets, nuspec-like metadata, or files that define what a package publishes -- **Build/tooling** — CI workflows, container definitions, build scripts -- **Documentation publishing** — doc-site navigation, generated-doc assets, site branding, or files whose main job is to make published docs render correctly -- **Community health/release communication** — changelogs, support/contribution/community defaults, and other files whose main audience is humans reading repo health or release status; this bucket normally maps to `💬` +- **Package/publish metadata** — release-note definitions, pack/publish targets, nuspec-like metadata, or files that define what a package publishes +- **Build/tooling** — CI workflows, container definitions, build scripts +- **Documentation publishing** — doc-site navigation, generated-doc assets, site branding, or files whose main job is to make published docs render correctly +- **Community health/release communication** — changelogs, support/contribution/community defaults, and other files whose main audience is humans reading repo health or release status; this bucket normally maps to `💬` These categories are examples, not a fixed taxonomy. Reuse the *rationale* behind them even when another repo uses different filenames or technologies. @@ -290,89 +294,31 @@ For every proposed commit, verify that all files share the same *rationale*. Pre - The **explanation for why each file changed** differs materially - One file changed because of an **operational/infrastructure decision** and another because of a **framework or API change** -When only two files changed but their rationales differ, **explicitly state that two commits are warranted** in the commit plan. Small file count does not justify bundling. - -#### Single-category context quality gate - -When more than one file is changed and your first classification puts every changed file into one semantic category or commit bucket, stop before Step 3 and run this gate. Exactly one changed file is the only fast-path exception; skip this gate for that case. - -Ask yourself explicitly: **“Did I actually read the whole `git-visual-commits` skill through EOF in this session before classifying this change?”** A metadata preview, remembered rule, or partial read is a failed answer. If the answer is no or uncertain, read `SKILL.md` from its first line through EOF and restart Step 1 and Step 2. - -Then re-check the complete `git status`, `git diff`, and applicable staged diff; enumerate every changed path; explain each path's rationale, audience, and lifecycle; and consider whether any path belongs to a different category such as documentation, configuration, tooling, validation, tests, or release communication. Re-read `references/commit-language.md` before confirming the category and emoji. - -Only keep one category after this audit if every path still has one rationale. Put a visible line in the commit plan such as `Quality gate: 3 files, one category retained; full skill read, full diff review, per-file rationale check, and alternative-category check confirmed.` If any check fails or any file has a materially different intent, split the groups and rerun the normal validation. `yolo` and `auto` do not bypass this gate. - -#### Commit body guidance - -Unless **no-body mode** is active, every commit includes a body explaining the *why*: - -- **Config/environment commits** → explain the operational intent (e.g. "Switch to shared-runner testing strategy with multi-image matrix") -- **Test assertion changes** → explain why the expectation changed (e.g. "net11 changed the default precision for DateTime, updating expected value") -- **Refactors** → explain what motivated the restructuring -- **New features** → explain the purpose and scope -- **Bug fixes** → explain what was broken and how this fixes it - -Common groupings: -- New repo-managed skill or workflow introduction together -- Existing skill refactor or extraction together -- Dependency/version baseline updates together -- Package/publish metadata together -- Config/setup files together (app host, bootstrapping) -- Environment and infrastructure config together (test runners, CI matrix, container settings) -- Documentation publishing fixes together -- Community health or release communication docs together -- New feature or module code together -- Data contracts, types, and interfaces together -- Database models, migrations, and schema changes together -- Test logic and assertions together (when they share the same rationale) -- Documentation and inline comments together - -When in doubt, one commit per "thing that changes" is better than one big commit. - -#### Mixed-scope guard - -After grouping, validate each proposed commit. If a single commit contains files from **three or more different categories** from Step 2 (e.g. docs + package props + solution files + API removals), **force a split**. A commit that touches documentation, build configuration, and source code at the same time is almost always an umbrella commit that should be broken apart. - -This guard runs unconditionally — including in auto-approval mode. - -#### Documentation separation rule - -Documentation files (`CHANGELOG.md`, `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, release notes) are **separate-by-default**. They only belong in the same commit as non-doc files when the commit is explicitly documentation-focused (e.g. `📝 add api usage guide` where the docs are the point, not a side effect). +When only two files changed but their rationales differ, **explicitly state that two commits are warranted** in the commit plan. Small file count does not justify bundling. -#### Release-adjacent splitting rule +#### Single-category context quality gate -Do not treat "all of this supports the release" as one commit. Release-adjacent work often spans different audiences and lifecycle roles that deserve separate history: +When more than one file is changed and your first classification puts every changed file into one semantic category or commit bucket, stop before Step 3 and run this gate. Exactly one changed file is the only fast-path exception; skip this gate for that case. -- **Dependency/version baselines** — version alignment or runner baseline changes -- **Community health/release communication** — changelogs and human-facing repo health docs -- **Package/publish metadata** — package release-note definitions, `.nuget/*/PackageReleaseNotes.txt`, and publish targets; this bucket normally maps to `📦` -- **Documentation publishing** — DocFX navigation, branding, or publishing assets -- **CI/automation** — workflows and helper scripts used only by automation +Ask yourself explicitly: **“Did I actually read the whole `git-visual-commits` skill through EOF in this session before classifying this change?”** A metadata preview, remembered rule, or partial read is a failed answer. If the answer is no or uncertain, read `SKILL.md` from its first line through EOF and restart Step 1 and Step 2. -These buckets are examples, not a fixed file map. The rule is the abstraction: split by purpose and audience, not by the fact that the changes landed together. +Then refresh the Step 1 inventory and review tracked diffs and untracked contents; explain each path's rationale, audience, and lifecycle; and consider whether any path belongs to a different category such as documentation, configuration, tooling, validation, tests, or release communication. Re-read `references/commit-language.md` before confirming the category and emoji. -Concrete example: if one diff updates `Directory.Build.targets`, `Directory.Packages.props`, or `testenvironments.json`, another diff updates CI scripts or workflow files such as `bump-nuget.py` or `.github/workflows/*.yml`, and another diff updates `CHANGELOG.md` plus `.nuget/*/PackageReleaseNotes.txt`, that is at least three intents: +Only keep one category after this audit if every path still has one rationale. Put a visible line in the commit plan such as `Quality gate: 3 files, one category retained; full skill read, full diff review, per-file rationale check, and alternative-category check confirmed.` If any check fails or any file has a materially different intent, split the groups and rerun the normal validation. `yolo` and `auto` do not bypass this gate. -- **Build system / dependency baseline** -- **CI or automation** -- **Release communication plus package metadata** +#### Mixed-scope guard -Do not collapse those into one commit, even if they were edited in the same round and all support the same release. Keep `.nuget/*/PackageReleaseNotes.txt` with the `📦` package/publish commit, not with the `💬` community-health commit. +After grouping, validate each proposed commit. If a single commit contains files from **three or more different categories** from Step 2 (e.g. docs + package props + solution files + API removals), **force a split**. A commit that touches documentation, build configuration, and source code at the same time is almost always an umbrella commit that should be broken apart. -#### Repo-aligned grouping example +This guard runs unconditionally — including in auto-approval mode. -When a repo like this one mixes skill changes, scaffold assets, validators, and repo docs, split them by intent: +#### Documentation separation rule -- **New repo-managed skill** — a newly introduced `skills//` folder and its local `evals/` or `references/` -- **Existing skill refactor** — extracting shared rules, renaming sections, or reorganizing an existing skill -- **Skill contract files** — `SKILL.md`, `FORMS.md`, `references/`, `evals/` -- **Template/runtime files** — `assets/`, scaffold helper scripts -- **Validation/tooling** — validator scripts, repo checks -- **Repo docs/rules** — `README.md`, `AGENTS.md`, `CONTRIBUTING.md` +Documentation files (`CHANGELOG.md`, `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, release notes) are **separate-by-default**. They only belong in the same commit as non-doc files when the commit is explicitly documentation-focused (e.g. `📝 add api usage guide` where the docs are the point, not a side effect). -Do not merge these into one commit unless the diff is truly single-purpose and the explanation still fits one sentence without using "and". +#### Detailed grouping rules -If a commit both introduces a brand-new skill and refactors an existing skill to support it, prefer separate commits. "Related" is not enough — the repo history should make it obvious which commit added the capability and which commit reorganized existing behavior around it. +Read [grouping-examples.md](references/grouping-examples.md) before finalizing groups. Apply its release-adjacent splitting rule and repository examples alongside the guards above. #### Rename vs removal distinction @@ -395,7 +341,7 @@ Before staging or committing anything, present the full commit plan to the user. Files: tests/Identity.Tests/ ``` -Before you render that plan, validate every proposed emoji and every proposed prefix against the inspected `references/commit-language.md`, then run `scripts/validate-commit-subject.ps1` for every exact subject. Fix failures before the user sees them. If the user did not explicitly ask for a conventional-commit combo, strip prefixes from the proposed subjects before presenting the plan. A plan containing an unvalidated subject is invalid, including in auto-approval mode. +Before you render that plan, validate every proposed emoji and every proposed prefix against the inspected `references/commit-language.md`, then run `scripts/validate-commit-subject.ps1` for every exact subject. Fix failures before the user sees them. If the user did not explicitly ask for a conventional-commit combo, strip prefixes from the proposed subjects before presenting the plan. A plan containing an unvalidated subject is invalid, including in auto-approval mode. If auto-approval is **not** active, Step 4 is a hard stop. Do not stage, do not commit, and do not treat silence or momentum as approval. @@ -405,11 +351,11 @@ If auto-approval is **not** active, Step 4 is a hard stop. Do not stage, do not Auto-committing: 🔧 build config → 🚚 rename auth to identity → ✅ identity tests → 💬 update changelog ``` -Even in auto-approval mode, surface the commit buckets explicitly before committing. Auto-approval removes the wait, not the planning step. - -The summary is status output, not a review request. Step 5 is mandatory in the same turn once its preconditions pass: never ask "Proceed with committing these groups?" (or an equivalent question), wait for a reply, or finish with a pending commit plan. +Even in auto-approval mode, surface the commit buckets explicitly before committing. Auto-approval removes the wait, not the planning step. + +The summary is status output, not a review request. Step 5 is mandatory in the same turn once its preconditions pass: never ask "Proceed with committing these groups?" (or an equivalent question), wait for a reply, or finish with a pending commit plan. -If the user did not narrow scope, the plan you surface must account for the full worktree rather than an arbitrarily chosen subset. +Reconcile the plan's exact path lists against a fresh Step 1 inventory before presenting it. Every in-scope path must belong to a commit group; splitting a file across groups must account for all its changes. Resolve missing paths before proceeding. Show a coverage line such as `Scope: 7 changed paths (3 untracked), 7 planned, 0 omitted.` Counts supplement the path comparison; equal counts alone do not prove coverage. Record explicit user exclusions separately. Apply this check in every mode, including single-file and auto-approved requests. **Otherwise**, wait for the user to confirm or adjust. They may say things like: - "Looks good" → proceed to stage and commit @@ -423,19 +369,20 @@ If the user's response is ambiguous, such as "4 is wrong now" or "that was fine #### Commit-message validation -Before committing, validate each message against its file list: - -- If the subject claims a breaking change or uses `💥`, most files must directly implement or document that break; split build, CI, environment, and unrelated tooling files. -- The subject must describe every staged file consistently. Split rename work from compatibility-shim removal. -- Validate the emoji meaning against the reference and the exact subject shape with the script. Correct mismatches before presenting the plan. +Before committing, validate each message against its file list: + +- If the subject claims a breaking change or uses `💥`, most files must directly implement or document that break; split build, CI, environment, and unrelated tooling files. +- The subject must describe every staged file consistently. Split rename work from compatibility-shim removal. +- Validate the emoji meaning against the reference and the exact subject shape with the script. Correct mismatches before presenting the plan. ### Step 5: Stage and commit each group -For each group: -1. `git add ` — be precise, don't use `git add .` unless everything belongs in one commit -2. Compose the commit message (see format above) -3. Run `scripts/validate-commit-subject.ps1` again against the exact subject that will be passed to Git. Add `-PrefixMode Required` only for an explicitly requested combo. Do not run Git unless the validator exits successfully. -4. Run the appropriate commit command: +For each group: +1. Refresh the inventory after approval and reconcile any changes with the plan. Stage each group's exact paths, including new files and deletions, with `git add -A -- `. Use literal pathspecs for names containing Git wildcard syntax. Do not use tracked-only staging such as `git add -u` or `git commit -a` as a substitute. + Compare `git diff --staged --name-status` and the full staged diff against the group's planned changes before committing. Account for pre-existing staged changes too; resolve missing or extra staged paths without discarding their contents. Keep staging and commits sequential because they share the index. +2. Compose the commit message (see format above) +3. Run `scripts/validate-commit-subject.ps1` again against the exact subject that will be passed to Git. Add `-PrefixMode Required` only for an explicitly requested combo. Do not run Git unless the validator exits successfully. +4. Run the appropriate commit command: - `git bot commit -m ""` — if the user asked the AI to commit (bot identity) - `git commit -m ""` — if the user asked to commit under their own identity - For `git our commit` — use whichever command matches the attribution the human chose @@ -449,35 +396,14 @@ When the body is just one short explanatory paragraph, prefer a single natural p ### Step 6: Verify -After committing, run `git log --oneline -5` to confirm the commit looks right. Read the stored subject with `git log -1 --format=%s` and run `scripts/validate-commit-subject.ps1` against that exact value. Then always run `git log -1 --format="%an <%ae>"` and verify that the author matches the requested identity mode before reporting success. Also run `git log -1 --format=%B` and verify the stored body contains readable prose with real line breaks, not literal escape sequences such as `\n`, and is not hard-wrapped mid-sentence just to satisfy a column limit. If any verification fails, amend the commit immediately instead of merely warning about it. +After each commit, compare its recorded paths (`git diff-tree --root --no-commit-id --name-status -r HEAD`) with the group, accounting for both sides of renames. After the last group, rerun `git status --short --untracked-files=all` and reconcile remaining paths with the original inventory and explicit exclusions. A full-worktree request is complete only when no in-scope changes remain, including untracked files. If a path was missed, review and include it through the same workflow under the existing authorization; do not silently leave it behind or claim completion. Report any concrete blocker or newly arriving change that prevents completion. + +After committing, run `git log --oneline -5` to confirm the commit looks right. Read the stored subject with `git log -1 --format=%s` and run `scripts/validate-commit-subject.ps1` against that exact value. Then always run `git log -1 --format="%an <%ae>"` and verify that the author matches the requested identity mode before reporting success. Also run `git log -1 --format=%B` and verify the stored body contains readable prose with real line breaks, not literal escape sequences such as `\n`, and is not hard-wrapped mid-sentence just to satisfy a column limit. If any verification fails, amend the commit immediately instead of merely warning about it. If verification fails because the commit path used the wrong author or ignored the requested alias, stop treating it as a message-tweaking problem. Correct the tool path first, preserve the worktree safely, and only then repair the commit. --- -## Good Examples - -``` -🎉 begin api project -✨ add submission endpoint module -🐛 handle null optional fields in dto -➕ add validation library -🐛 fix: handle null optional fields in dto ← only when combo mode was requested -``` - -## Bad Examples (and why) - -``` -feat: add submission endpoint ← "feat:" is not an allowed prefix -✨ Feat: Add Submission Module ← uppercase, "Feat:" not allowed -💬 Update CHANGELOG for v10.0.10 ← uppercase description beginning -💬 update changelog for v10.0.10 ← more than one separator space -📋 update changelog for v10.0.10 ← emoji is absent from the approved reference table -🎉 initial commit with all files ← vague, bundles everything -⚙️ config: setup api ← "config:" is not an allowed prefix -♻️ refactor: reorganize skill wording ← bad default if the user did not ask for the combo -``` +## Branching (for reference) -## Branching (for reference) - -When explicitly requested, use `[version]/[description]` (for example `v1.2.0/admin-dashboard`). Do not create, rename, or delete branches unless the user explicitly asks. +When explicitly requested, use `[version]/[description]` (for example `v1.2.0/admin-dashboard`). Do not create, rename, or delete branches unless the user explicitly asks. diff --git a/skills/git-visual-commits/evals/evals.json b/skills/git-visual-commits/evals/evals.json index 6ab5043..7fbda95 100644 --- a/skills/git-visual-commits/evals/evals.json +++ b/skills/git-visual-commits/evals/evals.json @@ -283,6 +283,41 @@ "Treats the full current worktree as scope because the user did not narrow it", "Does not replace bot identity with a human-authored commit plus a Co-authored-by trailer" ] + }, + { + "id": 26, + "prompt": "Please git commit, yolo. The worktree has an unstaged change to src/Router.cs, a staged change to config/routes.json, and new untracked src/RoutePolicy.cs and assets/route-map.svg. git diff --stat only lists src/Router.cs. All four files belong to this routing change.", + "expected_output": "A complete commit workflow accounting for all four paths, inspecting untracked contents and reconciling inventory, plan, index, recorded commits, and final status.", + "expectations": [ + "Uses git status --short --untracked-files=all or its NUL-delimited porcelain equivalent as the scope inventory instead of a diff summary", + "Inspects and includes both src/RoutePolicy.cs and assets/route-map.svg without asking permission merely because they are untracked", + "Accounts for the pre-existing staged config/routes.json change when verifying each commit group", + "Compares exact planned paths with the inventory and reports no omitted in-scope paths", + "Checks staged contents and recorded commit paths against each group and verifies no in-scope tracked or untracked changes remain" + ] + }, + { + "id": 27, + "prompt": "Please review my working tree and show the commit plan only. I changed README.md and added a new assets folder containing logo.svg and dark theme/banner.svg. Git is configured with status.showUntrackedFiles=no. A scratch file under ignored .cache/ also exists.", + "expected_output": "A read-only plan that explicitly discovers both new files, handles the path containing spaces, covers the README change, and excludes the ignored scratch file.", + "expectations": [ + "Overrides hidden untracked status with --untracked-files=all and enumerates individual files in new directories", + "Inspects both untracked assets and includes assets/logo.svg and assets/dark theme/banner.svg in the plan", + "Preserves the space-containing path as one filename", + "Excludes the ignored .cache file without force-adding it", + "Does not stage or commit for a plan-only request" + ] + }, + { + "id": 28, + "prompt": "Please git commit only the README.md change, yolo. src/Router.cs is modified and src/RoutePolicy.cs is untracked too; leave both alone.", + "expected_output": "A README-only commit that records the explicit exclusions and reports the remaining source changes accurately.", + "expectations": [ + "Limits the commit to README.md because the user explicitly narrowed scope", + "Leaves both src/Router.cs and src/RoutePolicy.cs out of the commit and preserves their contents", + "Reconciles final status against the explicit exclusions instead of requiring an entirely clean worktree", + "Does not claim the entire worktree is clean when excluded paths remain" + ] } ] } diff --git a/skills/git-visual-commits/references/grouping-examples.md b/skills/git-visual-commits/references/grouping-examples.md new file mode 100644 index 0000000..6276269 --- /dev/null +++ b/skills/git-visual-commits/references/grouping-examples.md @@ -0,0 +1,88 @@ +# Commit grouping examples + +#### Release-adjacent splitting rule + +Do not treat "all of this supports the release" as one commit. Release-adjacent work often spans different audiences and lifecycle roles that deserve separate history: + +- **Dependency/version baselines** — version alignment or runner baseline changes +- **Community health/release communication** — changelogs and human-facing repo health docs +- **Package/publish metadata** — package release-note definitions, `.nuget/*/PackageReleaseNotes.txt`, and publish targets; this bucket normally maps to `📦` +- **Documentation publishing** — DocFX navigation, branding, or publishing assets +- **CI/automation** — workflows and helper scripts used only by automation + +These buckets are examples, not a fixed file map. The rule is the abstraction: split by purpose and audience, not by the fact that the changes landed together. + +Concrete example: if one diff updates `Directory.Build.targets`, `Directory.Packages.props`, or `testenvironments.json`, another diff updates CI scripts or workflow files such as `bump-nuget.py` or `.github/workflows/*.yml`, and another diff updates `CHANGELOG.md` plus `.nuget/*/PackageReleaseNotes.txt`, that is at least three intents: + +- **Build system / dependency baseline** +- **CI or automation** +- **Release communication plus package metadata** + +Do not collapse those into one commit, even if they were edited in the same round and all support the same release. Keep `.nuget/*/PackageReleaseNotes.txt` with the `📦` package/publish commit, not with the `💬` community-health commit. + +#### Repo-aligned grouping example + +When a repo like this one mixes skill changes, scaffold assets, validators, and repo docs, split them by intent: + +- **New repo-managed skill** — a newly introduced `skills//` folder and its local `evals/` or `references/` +- **Existing skill refactor** — extracting shared rules, renaming sections, or reorganizing an existing skill +- **Skill contract files** — `SKILL.md`, `FORMS.md`, `references/`, `evals/` +- **Template/runtime files** — `assets/`, scaffold helper scripts +- **Validation/tooling** — validator scripts, repo checks +- **Repo docs/rules** — `README.md`, `AGENTS.md`, `CONTRIBUTING.md` + +Do not merge these into one commit unless the diff is truly single-purpose and the explanation still fits one sentence without using "and". + +If a commit both introduces a brand-new skill and refactors an existing skill to support it, prefer separate commits. "Related" is not enough — the repo history should make it obvious which commit added the capability and which commit reorganized existing behavior around it. + + +#### Commit body guidance + +Unless **no-body mode** is active, every commit includes a body explaining the *why*: + +- **Config/environment commits** → explain the operational intent (e.g. "Switch to shared-runner testing strategy with multi-image matrix") +- **Test assertion changes** → explain why the expectation changed (e.g. "net11 changed the default precision for DateTime, updating expected value") +- **Refactors** → explain what motivated the restructuring +- **New features** → explain the purpose and scope +- **Bug fixes** → explain what was broken and how this fixes it + +Common groupings: +- New repo-managed skill or workflow introduction together +- Existing skill refactor or extraction together +- Dependency/version baseline updates together +- Package/publish metadata together +- Config/setup files together (app host, bootstrapping) +- Environment and infrastructure config together (test runners, CI matrix, container settings) +- Documentation publishing fixes together +- Community health or release communication docs together +- New feature or module code together +- Data contracts, types, and interfaces together +- Database models, migrations, and schema changes together +- Test logic and assertions together (when they share the same rationale) +- Documentation and inline comments together + +When in doubt, one commit per "thing that changes" is better than one big commit. + + +## Good Examples + +``` +🎉 begin api project +✨ add submission endpoint module +🐛 handle null optional fields in dto +➕ add validation library +🐛 fix: handle null optional fields in dto ← only when combo mode was requested +``` + +## Bad Examples (and why) + +``` +feat: add submission endpoint ← "feat:" is not an allowed prefix +✨ Feat: Add Submission Module ← uppercase, "Feat:" not allowed +💬 Update CHANGELOG for v10.0.10 ← uppercase description beginning +💬 update changelog for v10.0.10 ← more than one separator space +📋 update changelog for v10.0.10 ← emoji is absent from the approved reference table +🎉 initial commit with all files ← vague, bundles everything +⚙️ config: setup api ← "config:" is not an allowed prefix +♻️ refactor: reorganize skill wording ← bad default if the user did not ask for the combo +```