diff --git a/.github/workflows/validate-skill-templates.yml b/.github/workflows/validate-skill-templates.yml index 2e26302..e51212a 100644 --- a/.github/workflows/validate-skill-templates.yml +++ b/.github/workflows/validate-skill-templates.yml @@ -14,10 +14,11 @@ jobs: timeout-minutes: 5 strategy: fail-fast: false - max-parallel: 13 + max-parallel: 14 matrix: include: - { name: templates, script: scripts/validate-skill-templates.ps1, suite: Templates } + - { name: pr, script: scripts/validate-skill-templates.ps1, suite: Pr } - { name: preparation, script: scripts/validate-skill-templates.ps1, suite: Preparation } - { name: runners, script: scripts/validate-skill-templates.ps1, suite: Runners } - { name: docfx, script: scripts/validate-skill-templates.ps1, suite: Docfx } diff --git a/CHANGELOG.md b/CHANGELOG.md index f375c71..c85f260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ 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.11.2] - 2026-10-03 + +This is a patch release that supports PR publication from branches without upstream tracking, refines changelog and NuGet release-note workflows, and allows Unicode emoji choices beyond the bundled commit-language tables. + +### Added + +- `git-remote-pr` supports publishing clean feature branches without tracking to GitHub `origin` or the sole GitHub remote under the exact current branch name; the preview and approval ID bind upstream setup, execution establishes tracking with a normal push, and post-push checks verify the published branch and tracking before PR metadata writes. + +### Changed + +- `git-keep-a-changelog` yolo/auto mode requires an explicit version or version-prefixed branch and omits the `Unreleased` heading and footer link on creation, updates, and reruns; existing entries must be reconciled into the concrete release, with unresolved content blocking edits, +- `git-nuget-release-notes` defaults to the full branch delta against the integration-branch merge-base, treats same-named tracking branches as synchronization targets, honors explicit ranges, and regenerates existing target-version drafts without narrowing scope to unpushed commits, +- `git-nuget-release-notes` covers every packable `src/` project by default, including unchanged packages, requiring a complete version and availability block with non-empty ALM; it uses the prescribed default ALM text when no package-specific ALM outcomes survive and verifies coverage while preserving older blocks, +- `git-visual-commits` and `git-visual-squash-summary` emoji guidance treats bundled tables as examples and honors user choices and repository conventions; commit-subject validation accepts a single Unicode emoji or symbol sequence beyond those tables while retaining spacing, lowercase, prefix, and length checks, +- Expanded deterministic repository assertions, commit-subject and PR regression tests, and versioned eval cases for the revised publication, changelog, NuGet coverage, and emoji-selection contracts. + ## [0.11.1] - 2026-09-29 This is a patch release delivering validation hardening for `git-remote-release` with stricter em-dash and alert-block enforcement, comprehensive test coverage for format compliance, Microsoft.Testing.Platform test runner support in `dotnet-remote-testing`, clarified yolo/auto approval behavior in `git-remote-pr`, and visual skill identification with hero images. @@ -747,6 +763,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.11.2]: https://github.com/codebeltnet/agentic/compare/v0.11.1...v0.11.2 [0.11.1]: https://github.com/codebeltnet/agentic/compare/v0.11.0...v0.11.1 [0.11.0]: https://github.com/codebeltnet/agentic/compare/v0.10.1...v0.11.0 [0.10.1]: https://github.com/codebeltnet/agentic/compare/v0.10.0...v0.10.1 diff --git a/README.md b/README.md index 3b42189..2c37c24 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ pwsh -NoProfile -File ./scripts/validate-local.ps1 The scheduler reads every suite from `.github/workflows/validate-skill-templates.yml`, so local and CI coverage stay aligned, and runs each one in a separate PowerShell 7 process with bounded concurrency, a streamed per-suite log, isolated fixtures, and per-suite exit codes. It enforces a per-suite timeout by killing the whole process tree, keeps running the remaining suites after a failure so one run reports every problem, and writes `summary.json` with the coverage counts next to the logs. It exits non-zero unless every CI-matrix suite exited zero and printed its terminal success marker, so a timeout or a silently skipped check is never reported as a pass. Use `-ListSuites` to print the resolved matrix, and `-MaxConcurrency`, `-TimeoutSeconds`, and `-DeadlineSeconds` to tune a run. Sequentially invoking the aggregate script or looping over the matrix one entry at a time is 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. 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 default to focused checks for the changed skill or component. Each full local run of `scripts/validate-local.ps1` requires explicit human approval after its scope and cost are explained. Generic requests to test or finalize work do not grant that approval. Focused checks satisfy the local completion gate when a full run has not been approved. This partition changes scheduling, not the model-free validation contract. +CI runs the complete validation set in 14 independent jobs with a five-minute budget per job. Template checks, PR workflow regressions, 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 Pr`, `-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 default to focused checks for the changed skill or component. Each full local run of `scripts/validate-local.ps1` requires explicit human approval after its scope and cost are explained. Generic requests to test or finalize work do not grant that approval. Focused checks satisfy the local completion gate when a full run has not been approved. This partition changes scheduling, not the model-free validation contract. ## Install a skill @@ -143,8 +143,8 @@ When repo-managed skills author Markdown, each prose paragraph and list item sta | Skill | Description | |-------|-------------| | [git-visual-commits](skills/git-visual-commits/SKILL.md) | AI-driven git commit workflow with authoritative routing for `git bot commit`, `git commit`, and `git our commit`, including the exact `Please do a git bot commit yolo` form. It locks the requested identity, treats yolo/auto only as scoped auto-approval modifiers, never as the commit message, and does not hand commit execution to changelog or release-note skills. It uses deterministically validated emoji-first subjects, optional conventional prefixes only on explicit request, full-worktree semantic grouping unless narrowed, a visible multi-file single-category quality gate, commit bodies by default, and post-commit identity/body verification. Multi-file plans that initially collapse to one category also require a visible full-context quality gate; one-file changes keep the fast path. Stack-agnostic. | -| [git-keep-a-changelog](skills/git-keep-a-changelog/SKILL.md) | Git-aware Keep a Changelog companion selected only for explicit changelog or release-note intent. Bare yolo/auto and commit-execution requests such as `git bot commit yolo` do not activate it; those words modify autonomy only after changelog intent is established. Bundled deterministic resolvers separate branch-unique commit history from merge-base-to-`HEAD` net diffs, exclude the previous-release or comparison boundary, fail on base-history bleed, and classify explicit path-backed release entities as `Added`, `Removed`, `Changed`, or `Unchanged`. The skill establishes each user-facing release entity against the base before section classification. It asks a mandatory `Yes / No / Custom` question before including pending worktree changes in ordinary concrete-release drafts, includes staged, unstaged, and untracked work automatically only in scoped yolo/auto mode, creates missing changelogs, writes SemVer-aware highlights, maintains compare-link footers, preserves natural prose wrapping, and curates surviving outcomes instead of dumping raw commit logs. | -| [git-nuget-release-notes](skills/git-nuget-release-notes/SKILL.md) | Git-aware NuGet release-notes companion for .NET repos that keep cumulative `.nuget/{ProjectName}/PackageReleaseNotes.txt` files. Discovers packable `src/` projects, resolves concrete package version and availability, creates missing files when needed, reduces each package to its surviving base-to-`HEAD` delta before classifying history, and establishes each package capability against the base so pre-release refinements and fixes to a new capability remain one `ADDED` New Feature. It writes per-package `ALM` / `Breaking Changes` / `New Features` / `Improvements` / `Bug Fixes` style notes from final package state plus supporting commit context instead of dumping commit subjects. | +| [git-keep-a-changelog](skills/git-keep-a-changelog/SKILL.md) | Git-aware Keep a Changelog companion selected only for explicit changelog or release-note intent. Bare yolo/auto and commit-execution requests such as `git bot commit yolo` do not activate it; both keywords enable identical autonomy and omit the `[Unreleased]` heading and footer link only after changelog intent is established, requiring an explicit version or version-prefixed branch. Bundled deterministic resolvers separate branch-unique commit history from merge-base-to-`HEAD` net diffs, exclude the previous-release or comparison boundary, fail on base-history bleed, and classify explicit path-backed release entities as `Added`, `Removed`, `Changed`, or `Unchanged`. The skill establishes each user-facing release entity against the base before section classification. It asks a mandatory `Yes / No / Custom` question before including pending worktree changes in ordinary concrete-release drafts, includes staged, unstaged, and untracked work automatically only in scoped yolo/auto mode, creates missing changelogs, writes SemVer-aware highlights, maintains compare-link footers, preserves natural prose wrapping, and curates surviving outcomes instead of dumping raw commit logs. | +| [git-nuget-release-notes](skills/git-nuget-release-notes/SKILL.md) | Git-aware NuGet release-notes companion for .NET repos that keep cumulative `.nuget/{ProjectName}/PackageReleaseNotes.txt` files. Uses the full current branch against its integration branch by default, treats same-named tracking branches as sync targets, and honors explicit ranges. A version-only invocation covers every packable `src/` project, including unchanged packages: each gets a complete version/availability block and non-empty ALM, using the required default text when no specific ALM outcomes exist. It creates missing files, reduces each package to its surviving base-to-`HEAD` delta before classifying history, and establishes each package capability against the base so pre-release refinements and fixes to a new capability remain one `ADDED` New Feature. It regenerates existing target-version blocks and verifies complete package coverage before handing the notes back for review. | | [git-nuget-readme](skills/git-nuget-readme/SKILL.md) | Git-aware NuGet README companion for .NET repos that advertise a package from `src/`. Resolves the real packable project the README should sell, combines git history with actual package metadata, source capabilities, and relevant tests when feasible, preserves honest badge/docs/contributing sections, and writes a forthcoming, adoption-friendly `README.md` with repo-derived branding, clear value, install, framework-support, and quick-start guidance. | | [git-visual-squash-summary](skills/git-visual-squash-summary/SKILL.md) | Non-mutating grouped-summary companion to `git-visual-commits`. Covers every surviving change on the full current feature branch across all contributors, using the merge-base with the repository base branch. Inventories every changed path and distinct outcome, recovers truncated diffs, and reconciles coverage before returning compact lowercase-start lines. No total line cap. Merges overlap and removes reverted churn while preserving small independent corrections, technical identifiers, and distinct dependency/version changes. Returns the summary directly without unnecessary scope or approval questions. | | [skill-creator-agnostic](skills/skill-creator-agnostic/SKILL.md) | **⚠️ Deprecated** — no longer maintained and retained only for backward compatibility until **1.0.0**. Do not use it for new skill-authoring work; use Anthropic `skill-creator` together with this repository's `AGENTS.md`. | @@ -155,7 +155,7 @@ When repo-managed skills author Markdown, each prose paragraph and list item sta | [trunk-first-repo](skills/trunk-first-repo/SKILL.md) | Initialize a git repository following [scaled trunk-based development](https://trunkbaseddevelopment.com/#scaled-trunk-based-development). Seeds an empty `main` branch, creates a versioned feature branch (`v0.1.0/init`), confirms configured remotes in its post-init summary, and supports a guarded later `push remote ` mode that checks the feature-branch/empty-main state before pushing `main` ahead of the first feature branch so content still reaches main only through peer-reviewed pull requests. | | [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` section whose first summary line begins `This release `, follows it with curated dash bullets using bold lead-ins and actual explanatory prose, optionally inserts supported GitHub alert blocks, preserves exact verified `Sources:` entries with contributor-complete GitHub logins, keeps em-dash bans scoped to authored prose rather than verbatim source titles, and ends with the full changelog compare link. | -| [git-remote-pr](skills/git-remote-pr/SKILL.md) | Create or refresh a GitHub PR from the complete committed `base...head` changeset. Read-only preparation links a Markdown preview containing the exact proposed title and complete description, assignee, push need, and planned writes before approval; normal execution requires the exact displayed `approve APR-...` phrase and rechecks the write intent, approval-bound normalized review hash, and final preview hash. Material drift ends the approval transaction without regeneration; an explicit refresh starts a new workspace and approval ID. Same-request `yolo`/`auto` still shows the preview but skips the approval wait, executing only the just-presented ID. Existing PR bodies are rebuilt from the current final text diff into concise thematic summaries, with binary assets represented only by paths, statuses, and metadata. Case-insensitive theme coverage reports all mismatches together, and failures remain visible in noninteractive hosts. Draft corrections reuse one workspace. Dirty worktrees, missing or mismatched upstream tracking, divergent branches, and failed post-write checks block completion. Uses Git and `gh`, without Copilot or a browser. | +| [git-remote-pr](skills/git-remote-pr/SKILL.md) | Create or refresh a GitHub PR from the complete committed `base...head` changeset. Read-only preparation links a Markdown preview containing the exact proposed title and complete description, assignee, push need, and planned writes before approval; normal execution requires the exact displayed `approve APR-...` phrase and rechecks the write intent, approval-bound normalized review hash, and final preview hash. Material drift ends the approval transaction without regeneration; an explicit refresh starts a new workspace and approval ID. Same-request `yolo`/`auto` still shows the preview but skips the approval wait, executing only the just-presented ID. Existing PR bodies are rebuilt from the current final text diff into concise thematic summaries, with binary assets represented only by paths, statuses, and metadata. Case-insensitive theme coverage reports all mismatches together, and failures remain visible in noninteractive hosts. Draft corrections reuse one workspace. Untracked branches preview publication to GitHub origin (or the sole GitHub remote) under the current branch name, then establish tracking during execution. Dirty worktrees, ambiguous remotes, incomplete or mismatched tracking, divergent branches, and failed post-write checks block completion. Uses Git and `gh`, without Copilot or a browser. | | [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. | @@ -387,7 +387,7 @@ The skill establishes each outcome's identity, scope, before/after state, and su - **Version-aware by branch** — uses a branch prefix like `v0.3.0/...` as the release heading hint when present - **Mandatory pending-worktree gate** — when a concrete release has uncommitted changes, the skill must ask a short `Yes / No / Custom` confirmation question before folding them into the changelog draft, with a `FORMS.md` definition that compatible hosts can render as native choices - **Trigger isolation** — yolo/auto modifies an explicit changelog request but never activates this skill for `git bot commit yolo` or another commit-execution request -- **Scope-safe yolo mode** — includes staged, unstaged, and untracked work automatically without changing the committed-history boundary +- **Scope-safe yolo/auto mode** — both keywords include staged, unstaged, and untracked work automatically without changing the committed-history boundary; omit the `[Unreleased]` heading and footer link on creation, updates, and reruns, while preserving concrete-version compare links and safely reconciling existing entries. Requires an explicit version or version-prefixed branch - **SemVer-aware highlight** — always writes a short release TL;DR that explicitly says `major`, `minor`, or `patch` - **Creates the file when needed** — seeds a compliant `CHANGELOG.md` if the repo does not have one yet - **Natural prose** — preserves human-readable line breaks without any fixed-width wrapping target @@ -403,7 +403,10 @@ Repo-wide changelogs are useful, but NuGet packages often need package-scoped re **git-nuget-release-notes** reads the actual git history and net diff per packable `src/` project, resolves the package version and target framework availability, then updates the package-note files directly for review. - **Per-package, not repo-wide** — writes one truthful release block per publishable assembly/package +- **Complete release coverage** — version-only requests update every packable `src/` project, including unchanged packages; each gets resolved availability and non-empty ALM with the required default text when needed - **Concrete package metadata** — resolves `Version:` and `Availability:` from the branch/project instead of inventing placeholders +- **Full branch by default** — compares against the integration branch instead of only the unpushed commits; a version-only invocation edits the notes without an unnecessary range question +- **Draft regeneration** — recomputes an existing target-version block from the entire resolved delta while preserving older released blocks; explicit ranges still override the default - **Package delta first** — resolves public APIs, dependency versions, TFMs, and package metadata from base to `HEAD` before using history as supporting context - **Reverted churn disappears** — temporary upgrades, removed-then-restored APIs, and other cancelled work do not reach the final release block - **Current codebelt format** — follows the established `ALM`, `Breaking Changes`, `New Features`, `Improvements`, `Bug Fixes`, and optional `References` blueprint @@ -623,7 +626,7 @@ Most repositories start with `git init` followed by committing everything direct **git-remote-pr** prepares a reviewable pull request from every committed change between the resolved base branch and the current head. It inventories commits, changed paths, renames, binaries, and the final patch, then collapses that evidence into a concise opening paragraph followed by bold thematic headings with trailing colons and compact bullets. Related helpers, validation cases, references, formatting changes, and assets become shared reviewer-facing outcomes; complete internal file coverage does not require a prose inventory. The planner rejects malformed default bodies before preview: exactly one opening paragraph, bold colon-ended themes, dash bullets under every theme, and no concluding summary. A deterministic approval ID binds the complete prepared write intent plus the SHA-256 hash of the complete preview with only its two generated approval-ID fields normalized to `{{APPROVAL_ID}}`. A separate SHA-256 hash checks the exact final `preview.md`, including its approval instruction; changing that hash cannot authorize altered preview text. Normal mode requires `approve APR-...` for that preview; generic approval cannot execute or regenerate it. Same-request `yolo`/`auto` still shows the preview but treats that approval phrase as status only, continuing without asking for or waiting on another reply. State drift permanently invalidates the transaction and preserves its artifacts. Only an explicit refresh request starts fresh preparation in a new workspace with a new approval ID. Repository templates retain precedence. It uses `git` and `gh` without GitHub Copilot or browser assistance. -Normal requests stop after showing the proposed title, reviewer-oriented description summary, assignee, push requirement, and exact remote writes. A `yolo` or `auto` attached to the same PR request still shows that preview, but it does not ask for or wait on `approve APR-...`; it proceeds immediately with the just-presented plan. Existing open PRs are refreshed from the current complete changeset, and the coverage checks still require every changed file to map to a theme represented in the body even when many files collapse into a few sections. Execution refuses plans whose approved title, ready/draft state, body, evidence, or repository snapshot changed after approval. Post-write checks verify the title, body, draft state, assignment, head SHA, and GitHub file inventory. Dirty worktrees, missing upstream tracking, renamed-branch tracking mismatches, and divergent remote branches block the workflow instead of silently retargeting the PR head. Commit creation remains a separate `git-visual-commits` request; release-note writing remains `git-remote-release`. +Normal requests stop after showing the proposed title, reviewer-oriented description summary, assignee, push requirement, and exact remote writes. A `yolo` or `auto` attached to the same PR request still shows that preview, but it does not ask for or wait on `approve APR-...`; it proceeds immediately with the just-presented plan. Existing open PRs are refreshed from the current complete changeset, and the coverage checks still require every changed file to map to a theme represented in the body even when many files collapse into a few sections. Execution refuses plans whose approved title, ready/draft state, body, evidence, or repository snapshot changed after approval. Post-write checks verify the title, body, draft state, assignment, head SHA, and GitHub file inventory. A branch that has never been pushed needs no preliminary tracking setup: preparation compares local HEAD against the base and previews a normal push to GitHub origin (or the sole GitHub remote) under the current branch name, including upstream tracking. Same-request `create yolo` executes that publication and PR creation without another reply. Dirty worktrees, ambiguous publication remotes, incomplete tracking, renamed-branch tracking mismatches, and divergent remote branches still block the workflow. Commit creation remains a separate `git-visual-commits` request; release-note writing remains `git-remote-release`. ### Why git-remote-release? diff --git a/scripts/eval-runners/tests/test-progress-coalescing.ps1 b/scripts/eval-runners/tests/test-progress-coalescing.ps1 index accac37..de61400 100644 --- a/scripts/eval-runners/tests/test-progress-coalescing.ps1 +++ b/scripts/eval-runners/tests/test-progress-coalescing.ps1 @@ -107,7 +107,23 @@ function Get-RelayLogEvents { <# Relay events in JSONL (origin=relay). #> param([object[]]$Events) return @($Events | Where-Object { [string]$_.origin -eq 'relay' }) -} +} + +function Invoke-CapturedHeartbeatTick { + param([Parameter(Mandatory = $true)][object]$Child) + + $operatorErrorWriter = [System.IO.StringWriter]::new([Globalization.CultureInfo]::InvariantCulture) + $originalErrorWriter = [Console]::Error + try { + [Console]::SetError($operatorErrorWriter) + Invoke-RunnerChildHeartbeatTick -Child $Child + } finally { + try { [Console]::Error.Flush() } catch { } + [Console]::SetError($originalErrorWriter) + } + + return @(($operatorErrorWriter.ToString() -split "`r?`n") | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) +} try { # ------------------------------------------------------------------ @@ -158,22 +174,61 @@ for ($i = 0; $i -lt 14; $i++) { } [Console]::Out.Write('{"status":"completed"}') '@ - $activeRelay = Invoke-CoalescingChild -ScriptPath $activeRelayScript -WorkerId 'arm-3-active-relay' -HeartbeatSeconds 0.25 - Assert-Equal 0 $activeRelay.ExitCode 'Test 3: active relay child exits cleanly' - $activeRelayConsoleLines = @(Get-RelayConsoleLines -Lines $activeRelay.OperatorLines) - $activeParentPeriodicLines = @(Get-ParentPeriodicLines -Lines $activeRelay.OperatorLines) - Assert-True ($activeRelayConsoleLines.Count -ge 8) "Test 3: relay events appear on console (got $($activeRelayConsoleLines.Count))" - # With relay emitting every 80ms and heartbeat at 250ms, parent heartbeats - # during active relay should be sparse: at most 1-2 (possibly 1 at launch - # before relay arrives, possibly 1 after relay stops). - Assert-True ($activeParentPeriodicLines.Count -le 3) "Test 3: parent periodic heartbeats suppressed during active relay (console count=$($activeParentPeriodicLines.Count), relay count=$($activeRelayConsoleLines.Count))" - # JSONL must still have parent periodic events even when console-suppressed. - $activeParentLogEvents = @(Get-ParentPeriodicLogEvents -Events $activeRelay.Events) - $activeRelayLogEvents = @(Get-RelayLogEvents -Events $activeRelay.Events) - Assert-True ($activeParentLogEvents.Count -ge 2) "Test 3: parent periodic events still in JSONL even when console-suppressed (got $($activeParentLogEvents.Count))" - Assert-True ($activeRelayLogEvents.Count -ge 8) "Test 3: relay events in JSONL (got $($activeRelayLogEvents.Count))" - - # ------------------------------------------------------------------ + $activeRelay = Invoke-CoalescingChild -ScriptPath $activeRelayScript -WorkerId 'arm-3-active-relay' -HeartbeatSeconds 0.25 + Assert-Equal 0 $activeRelay.ExitCode 'Test 3: active relay child exits cleanly' + $activeRelayConsoleLines = @(Get-RelayConsoleLines -Lines $activeRelay.OperatorLines) + Assert-True ($activeRelayConsoleLines.Count -ge 8) "Test 3: relay events appear on console (got $($activeRelayConsoleLines.Count))" + # The async child reader can be delayed by host load. Heartbeats emitted + # before its first relay is observed are correct, so test the policy below + # with controlled activity timestamps instead of asserting a wall-clock line + # count for this subprocess scenario. + $activeParentLogEvents = @(Get-ParentPeriodicLogEvents -Events $activeRelay.Events) + $activeRelayLogEvents = @(Get-RelayLogEvents -Events $activeRelay.Events) + Assert-True ($activeParentLogEvents.Count -ge 2) "Test 3: parent periodic events are retained in JSONL (got $($activeParentLogEvents.Count))" + Assert-True ($activeRelayLogEvents.Count -ge 8) "Test 3: relay events in JSONL (got $($activeRelayLogEvents.Count))" + + # Test 3b - Deterministic coalescing policy: a recent relay suppresses only + # the console heartbeat while retaining JSONL evidence; once relay activity + # is stale, the parent heartbeat is visible again. + $policyStartedUtc = [DateTime]::UtcNow.AddSeconds(-5) + $policyLogPath = Join-Path $testRoot 'coalescing-policy.jsonl' + $policyChild = [pscustomobject]@{ + Runner = 'relay-test' + WorkerId = 'arm-3-policy' + EvalId = 1 + Configuration = 'with_skill' + ProcessId = 12345 + Phase = 'model-cli' + Turn = $null + ProgressLogPath = $policyLogPath + HeartbeatSeconds = 0.25 + ProgressEnabled = $true + StartedUtc = $policyStartedUtc + DeadlineUtc = $policyStartedUtc.AddSeconds(30) + StdoutActivity = $null + StderrActivity = $null + LastHeartbeatUtc = [DateTime]::UtcNow.AddSeconds(-1) + LastRelayActivityUtc = [DateTime]::UtcNow + FirstStdoutSeen = $true + FirstStderrSeen = $true + LifecycleState = 'running' + } + $recentRelayConsoleLines = @(Invoke-CapturedHeartbeatTick -Child $policyChild) + Assert-Equal 0 $recentRelayConsoleLines.Count 'Test 3b: recent relay activity suppresses the console heartbeat' + $recentRelayLogEvents = @(Get-Content -LiteralPath $policyLogPath | ForEach-Object { $_ | ConvertFrom-Json }) + $recentRelayParentEvents = @(Get-ParentPeriodicLogEvents -Events $recentRelayLogEvents) + Assert-Equal 1 $recentRelayParentEvents.Count 'Test 3b: suppressed heartbeat remains in JSONL' + + $policyChild.LastHeartbeatUtc = [DateTime]::UtcNow.AddSeconds(-1) + $policyChild.LastRelayActivityUtc = [DateTime]::UtcNow.AddSeconds(-1) + $staleRelayConsoleLines = @(Invoke-CapturedHeartbeatTick -Child $policyChild) + $staleRelayParentLines = @(Get-ParentPeriodicLines -Lines $staleRelayConsoleLines) + Assert-Equal 1 $staleRelayParentLines.Count 'Test 3b: heartbeat resumes on the console after relay activity goes stale' + $allPolicyLogEvents = @(Get-Content -LiteralPath $policyLogPath | ForEach-Object { $_ | ConvertFrom-Json }) + $allPolicyParentEvents = @(Get-ParentPeriodicLogEvents -Events $allPolicyLogEvents) + Assert-Equal 2 $allPolicyParentEvents.Count 'Test 3b: visible heartbeat also remains in JSONL' + + # ------------------------------------------------------------------ # Test 4 - Relay stops, parent resumes: after relay goes quiet, parent # periodic heartbeat lines must resume on the console. # ------------------------------------------------------------------ diff --git a/scripts/test-validation-suites.ps1 b/scripts/test-validation-suites.ps1 index 6440a52..8f4a780 100644 --- a/scripts/test-validation-suites.ps1 +++ b/scripts/test-validation-suites.ps1 @@ -20,6 +20,8 @@ foreach ($script in @('scripts/validate-skill-templates.ps1', 'scripts/eval-runn $attribute = @($parameter.Attributes | Where-Object { $_.TypeName.Name -eq 'ValidateSet' })[0] $expected = @($attribute.PositionalArguments | ForEach-Object { $_.SafeGetValue() } | Where-Object { $_ -ne 'All' }) if ($script -eq 'scripts/validate-skill-templates.ps1') { + $prCheck = @($ast.FindAll({ param($node) $node -is [Management.Automation.Language.CommandAst] -and $node.GetCommandName() -eq 'Add-ValidationResult' -and $node.Extent.Text.Contains("-Name 'Git remote PR routing and deterministic workflow stay integrated'") }, $true)) + if ($prCheck.Count -ne 1 -or $prCheck[0].Extent.Text -notmatch "-Group 'Pr'") { throw 'PR workflow regressions must run exactly once in their own Pr suite so template builds can execute concurrently in CI and locally.' } # These two aggregate groups are expanded into their own test suites. $expected = @($expected | Where-Object { $_ -notin @('Conformance', 'Integrity') }) $groups = @($ast.FindAll({ param($node) $node -is [Management.Automation.Language.CommandAst] -and $node.GetCommandName() -eq 'Add-ValidationResult' }, $true) | ForEach-Object { diff --git a/scripts/tests/test-validate-local.ps1 b/scripts/tests/test-validate-local.ps1 index 6895826..14539e3 100644 --- a/scripts/tests/test-validate-local.ps1 +++ b/scripts/tests/test-validate-local.ps1 @@ -446,6 +446,7 @@ try { Assert-True -Condition ($LASTEXITCODE -eq 0) -Message 'expected suite listing to succeed' $entries = @($listed | Where-Object { [string]$_ -match ' -> ' }) Assert-True -Condition ($entries.Count -ge 1) -Message 'expected at least one CI suite' + Assert-True -Condition ($entries -contains 'pr -> scripts/validate-skill-templates.ps1 -Suite Pr') -Message 'expected PR regressions to be independently scheduled locally as in CI' foreach ($entry in $entries) { $parts = [string]$entry -split ' -> ' $scriptArguments = $parts[1] -split ' -Suite ' diff --git a/scripts/validate-skill-templates.ps1 b/scripts/validate-skill-templates.ps1 index 39c353e..5e7f313 100644 --- a/scripts/validate-skill-templates.ps1 +++ b/scripts/validate-skill-templates.ps1 @@ -2,7 +2,7 @@ param( [string]$Ref, [switch]$Full, [switch]$MetadataOnly, - [ValidateSet('All', 'Templates', 'Preparation', 'Runners', 'Conformance', 'Integrity', 'Docfx')] + [ValidateSet('All', 'Templates', 'Pr', 'Preparation', 'Runners', 'Conformance', 'Integrity', 'Docfx')] [string]$Suite = 'All' ) @@ -827,7 +827,7 @@ if ($MetadataOnly) { exit 0 } -Add-ValidationResult -Results $results -Name 'Git remote PR routing and deterministic workflow stay integrated' -Group 'Templates' -Action { +Add-ValidationResult -Results $results -Name 'Git remote PR routing and deterministic workflow stay integrated' -Group 'Pr' -Action { if (-not [string]::IsNullOrWhiteSpace($Ref)) { return } $prSkill = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/git-remote-pr/SKILL.md' -GitRef $Ref $agents = Get-FileText -RepoRoot $repoRoot -RelativePath 'AGENTS.md' -GitRef $Ref @@ -3065,6 +3065,8 @@ Add-ValidationResult -Results $results -Name 'Git visual commits skill enforces 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' Assert-Contains -Name 'git-visual-commits/references/commit-language.md' -Content $commitLanguage -Needle '#### Fallback: Extended Emoji Reference' + Assert-Contains -Name 'git-visual-commits/references/commit-language.md' -Content $commitLanguage -Needle 'These tables are selection guidance, not an allowlist.' + Assert-Contains -Name 'git-visual-commits/SKILL.md' -Content $skill -Needle 'Its emoji tables are examples, not an allowlist.' Assert-Contains -Name 'git-visual-commits/references/commit-language.md' -Content $commitLanguage -Needle 'Community health, changelog, release-status communication' Assert-Contains -Name 'git-visual-commits/references/commit-language.md' -Content $commitLanguage -Needle 'package release-note metadata' @@ -3072,7 +3074,8 @@ Add-ValidationResult -Results $results -Name 'Git visual commits skill enforces Assert-Contains -Name 'git-visual-commits/scripts/validate-commit-subject.ps1' -Content $subjectValidator -Needle '[System.Globalization.StringInfo]::ParseCombiningCharacters($Subject).Count' Assert-Contains -Name 'git-visual-commits/scripts/validate-commit-subject.ps1' -Content $subjectValidator -Needle 'Use exactly one ASCII space between the emoji and the following text.' Assert-Contains -Name 'git-visual-commits/scripts/validate-commit-subject.ps1' -Content $subjectValidator -Needle "elseif (`$description -cnotmatch '^\p{Ll}')" - Assert-Contains -Name 'git-visual-commits/scripts/validate-commit-subject.ps1' -Content $subjectValidator -Needle 'is not an approved entry in the bundled commit-language reference.' + Assert-Contains -Name 'git-visual-commits/scripts/validate-commit-subject.ps1' -Content $subjectValidator -Needle 'Subject must begin with one Unicode emoji sequence' + Assert-NotContains -Name 'git-visual-commits/scripts/validate-commit-subject.ps1' -Content $subjectValidator -Needle '$reference.Contains' Assert-Contains -Name 'git-visual-commits/scripts/validate-commit-subject.ps1' -Content $subjectValidator -Needle '$maxLength = 70' Assert-Contains -Name 'git-visual-commits/scripts/validate-commit-subject.ps1' -Content $subjectValidator -Needle 'the maximum is $maxLength.' Assert-Contains -Name 'git-visual-commits/scripts/test-commit-subject.ps1' -Content $subjectTests -Needle 'reported screenshot regression' @@ -3101,7 +3104,7 @@ Add-ValidationResult -Results $results -Name 'Git visual commits skill enforces Assert-Contains -Name 'git-visual-commits/evals/evals.json' -Content $evals -Needle 'Treats references/commit-language.md as a bundled skill resource rather than a repo-root path by default' Assert-Contains -Name 'git-visual-commits/evals/evals.json' -Content $evals -Needle 'Does not report a blocker solely because the current repository lacks a top-level references directory' Assert-Contains -Name 'git-visual-commits/evals/evals.json' -Content $evals -Needle 'Reads SKILL.md completely through EOF before any staging or commit command' - Assert-Contains -Name 'git-visual-commits/evals/evals.json' -Content $evals -Needle 'Rejects the proposed subject because 📋 is absent from the approved commit-language table' + Assert-Contains -Name 'git-visual-commits/evals/evals.json' -Content $evals -Needle 'not because 📋 is absent from the bundled table' Assert-Contains -Name 'git-visual-commits/evals/evals.json' -Content $evals -Needle 'Runs scripts/validate-commit-subject.ps1 before showing the corrected subject and again immediately before passing it to Git' Assert-Contains -Name 'git-visual-commits/evals/evals.json' -Content $evals -Needle 'Triggers the single-category context quality gate because more than one file is being placed in one category' Assert-Contains -Name 'git-visual-commits/evals/evals.json' -Content $evals -Needle 'Recognizes exactly one changed file as the explicit exception and skips the single-category context quality gate' @@ -3207,7 +3210,7 @@ Add-ValidationResult -Results $results -Name 'Git keep a changelog skill updates Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'If `CHANGELOG.md` does not exist, create a compliant one before' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Read full commit subjects and bodies before writing the changelog.' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'If the current branch starts with a version hint such as `v0.3.0/`,' - Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Otherwise, target `## [Unreleased]`.' + Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Otherwise, target `## [Unreleased]` unless yolo/auto mode is active;' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Always write a release highlight immediately below the target heading.' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'The release highlight must explicitly classify the release as `major`,' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle '## Mandatory Checkpoints' @@ -3227,7 +3230,13 @@ Add-ValidationResult -Results $results -Name 'Git keep a changelog skill updates Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Require `base_history_bleed` to be `false`.' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Do not append `^` or widen either range for a concrete release.' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'The comparison boundary is always excluded from a branch-derived release' - Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Yolo/auto changes pending-worktree handling only.' + Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Yolo/auto never widens committed history or includes the comparison boundary.' + Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle '**Omit `[Unreleased]` in yolo/auto mode.**' + Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Both keywords have identical behavior.' + Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'stop without editing and report that yolo/auto requires an explicit version or a version-prefixed branch.' + Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Before removing a populated section, reconcile its entries against the selected release evidence' + Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'verify that neither a `## [Unreleased]` heading nor an `[Unreleased]:` footer link remains' + Assert-NotContains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Yolo/auto changes pending-worktree handling only.' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Do not dump commit subjects verbatim into the changelog.' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'If `CHANGELOG.md` is missing, create it with the standard title,' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Always maintain the Keep a Changelog compare-link footer at the bottom of the file.' @@ -3267,6 +3276,10 @@ Add-ValidationResult -Results $results -Name 'Git keep a changelog skill updates Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Inserts the compare-link footer at the bottom when it is missing from an existing changelog' Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Treats the merge-base as an excluded boundary rather than the first commit of the concrete release' Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Does not let yolo mode widen committed history or include the v10.0.9 boundary commit' + Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Omits both the Unreleased heading and its footer link when creating a changelog in yolo/auto mode' + Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Removes the existing Unreleased heading and footer link without losing supported release outcomes' + Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Stops without editing when yolo/auto has no explicit version or branch version prefix' + Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Retains the standard Unreleased heading and footer link outside yolo/auto mode' Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Runs scripts/resolve-release-entity.ps1 for the path-backed dotnet-test entity and uses its Added classification' Assert-Contains -Name 'git-keep-a-changelog/evals/evals.json' -Content $evals -Needle 'Does not select git-keep-a-changelog from bare yolo wording inside a git bot commit request' Assert-Contains -Name 'git-keep-a-changelog/SKILL.md' -Content $skill -Needle 'Factual review: reread the written target entry and verify every factual clause against the before/after evidence' @@ -3317,12 +3330,41 @@ Add-ValidationResult -Results $results -Name 'Git summary skills reduce ranges t Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'Classify each user-facing package capability from whether it existed at the resolved base' Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle '`# Improvements` and `# Bug Fixes` require the affected capability or behavior to exist at the resolved base.' Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'Do not accumulate bullets from individual commits and deduplicate them afterward.' - Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle '`Newtonsoft.Json 13.0.3 -> 14.0.0 -> 13.0.3` -> no `# ALM` bullet.' + Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle '`Newtonsoft.Json 13.0.3 -> 14.0.0 -> 13.0.3` -> no dependency-specific `# ALM` bullet; keep the required default ALM block.' Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'Read the full commit bodies only after the cumulative delta is clear.' Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'A restored API or reverted dependency upgrade does not earn a section entry.' + Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'Treat a same-named tracking branch as a synchronization target, not the default release-note base.' + Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'Prefer the locally cached remote default branch' + Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'Preserve explicit range endpoints; for a branch comparison, resolve its merge-base with `HEAD`.' + Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'Do not ask the user to choose between the full branch and its latest commits when the default resolves safely.' + Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'Do not switch to a previous-release tag merely because a version was supplied.' + Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'The current target-version block is cached output, not a comparison baseline.' + Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'A version-only invocation covers every packable `src/` project, including unchanged projects.' + Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'An empty semantic delta suppresses change-specific bullets, never the release block.' + Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'If there are no package-specific ALM outcomes, use this exact default bullet:' + Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'Reuse the prior Availability line when the current target frameworks are verified unchanged.' + Assert-Contains -Name 'git-nuget-release-notes/SKILL.md' -Content $nugetSkill -Needle 'Before stopping, reconcile the selected project inventory against the written target-version blocks.' + $nugetFormat = Get-FileText -RepoRoot $repoRoot -RelativePath 'skills/git-nuget-release-notes/references/package-release-notes-format.md' -GitRef $Ref + Assert-Contains -Name 'package-release-notes-format.md' -Content $nugetFormat -Needle 'Every selected package receives a release block, even when its semantic delta is empty.' + foreach ($content in @($nugetSkill, $nugetFormat)) { + Assert-Contains -Name 'NuGet release-note default' -Content $content -Needle '- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)' + } + if ($nugetSkill.Contains('Base state and `HEAD` state are identical -> no entry.') -or $nugetSkill.Contains('Otherwise, focus on the projects affected by the requested range.')) { + throw 'NuGet release notes must not omit unchanged packages from the default release pass.' + } + if ($nugetSkill.Contains('Otherwise, compare the current branch to its upstream merge-base.')) { + throw 'NuGet release notes must not default to the tracking upstream, which can omit already-pushed release work.' + } Assert-Contains -Name 'git-nuget-release-notes/evals/evals.json' -Content $nugetEvals -Needle 'Omits the reverted `Newtonsoft.Json` change because the final version matches the base state' Assert-Contains -Name 'git-nuget-release-notes/evals/evals.json' -Content $nugetEvals -Needle 'Does not claim a breaking API removal for `WidgetClient.LegacySend()` because it was restored unchanged' Assert-Contains -Name 'git-nuget-release-notes/evals/evals.json' -Content $nugetEvals -Needle 'Does not place RetryPolicy under Improvements because it was refined before its first release' + Assert-Contains -Name 'git-nuget-release-notes/evals/evals.json' -Content $nugetEvals -Needle 'Uses the integration-branch merge-base instead of the same-named tracking upstream as the default base' + Assert-Contains -Name 'git-nuget-release-notes/evals/evals.json' -Content $nugetEvals -Needle 'Generates the requested 10.8.0 package notes without asking the user to confirm a safely resolved default range' + Assert-Contains -Name 'git-nuget-release-notes/evals/evals.json' -Content $nugetEvals -Needle 'Recomputes the existing 10.8.0 block from the full branch delta instead of treating its draft commit as the baseline' + Assert-Contains -Name 'git-nuget-release-notes/evals/evals.json' -Content $nugetEvals -Needle 'Honors the explicit origin/v1.1.0/feature..HEAD range even though its base is a same-named tracking branch' + Assert-Contains -Name 'git-nuget-release-notes/evals/evals.json' -Content $nugetEvals -Needle 'Writes the target version for every packable src project, including unchanged packages with existing or missing note files' + Assert-Contains -Name 'git-nuget-release-notes/evals/evals.json' -Content $nugetEvals -Needle 'Repairs an existing target-version feature block that lacks ALM instead of accepting its feature text alone as complete' + Assert-Contains -Name 'git-nuget-release-notes/evals/evals.json' -Content $nugetEvals -Needle 'Completes all selected package blocks even when the entire package delta is empty' Assert-Contains -Name 'git-visual-squash-summary/SKILL.md' -Content $squashSkill -Needle 'This skill answers one question: **What would this branch effectively do if it were squashed into one commit now?**' Assert-Contains -Name 'git-visual-squash-summary/SKILL.md' -Content $squashSkill -Needle 'History is evidence; the resulting state is truth.' diff --git a/skills/git-keep-a-changelog/SKILL.md b/skills/git-keep-a-changelog/SKILL.md index 312a626..44f2f9b 100644 --- a/skills/git-keep-a-changelog/SKILL.md +++ b/skills/git-keep-a-changelog/SKILL.md @@ -28,9 +28,10 @@ Read `FORMS.md` when pending worktree changes require user confirmation and the Only after this skill has been selected by explicit changelog or release-note intent, `yolo` or `auto` in that same request (case-insensitive) enables full-autonomy mode. Bare `yolo` / `auto`, `git bot commit yolo`, and other commit-execution requests do not activate this skill: -- **Skip Step 3's confirmation only.** Do not ask the confirmation question. Do not present the `Yes / No / Custom` gate. Still discover and inspect every pending change in Step 4. +- **Skip Step 3's confirmation.** Do not ask the confirmation question. Do not present the `Yes / No / Custom` gate. Still discover and inspect every pending change in Step 4. - **Include all pending changes automatically.** Staged, unstaged, and untracked files are all treated as part of the release scope without asking. -- **Keep committed history isolated.** Yolo changes only the pending-worktree decision; use the same resolved branch ranges and bleed guard as every other invocation. +- **Keep committed history isolated.** Use the same resolved branch ranges and bleed guard as every other invocation. +- **Omit `[Unreleased]` in yolo/auto mode.** When the request contains `yolo` or `auto` (case-insensitive), write a concrete release entry without a `## [Unreleased]` heading or `[Unreleased]:` footer link. Apply this to new and existing changelogs; do not recreate either on subsequent runs. Both keywords have identical behavior. - **Make all scope decisions independently.** The user has explicitly delegated judgment. Do not pause for input at any point in the workflow. - All other quality rules remain in force: the release highlight is still required, the SemVer classification is still required, bullet punctuation still applies, and the compare-link footer must still be maintained. @@ -50,7 +51,7 @@ Only after this skill has been selected by explicit changelog or release-note in - 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. - If a concrete target heading already exists but its matching `vX.Y.Z` tag does not, treat that heading as an unreleased draft and regenerate it from the resolved git result instead of preserving stale bullets as a second baseline. -- Otherwise, target `## [Unreleased]`. +- Otherwise, target `## [Unreleased]` unless yolo/auto mode is active; follow Step 2's concrete-version requirement instead. - Always write a release highlight immediately below the target heading. - The release highlight must explicitly classify the release as `major`, `minor`, or `patch`. - Use the standard Keep a Changelog section order: `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`. @@ -64,7 +65,7 @@ Only after this skill has been selected by explicit changelog or release-note in - **Never hard-wrap changelog prose.** Keep every paragraph and bullet item on one physical line, regardless of length. Do not insert line breaks to satisfy 80, 100, 120, or any other column width; rely on editor soft wrapping. Insert physical line breaks only between Markdown structures, and rejoin unnecessary wraps in prose you touch. Treat any arbitrary line break inside a paragraph or bullet as a formatting failure that must be corrected before completion. - End each bullet with `,` and end the last bullet in each section with `.`. - If pending worktree changes exist for a concrete release draft, do not silently include or exclude them. Ask the user first with a short `Yes / No / Custom` prompt. **Exception: in yolo/auto mode, include all pending changes automatically without asking.** -- Yolo/auto changes pending-worktree handling only. It never widens committed history or includes the comparison boundary. +- Yolo/auto never widens committed history or includes the comparison boundary. Both modes also omit `[Unreleased]`. - Do not dump commit subjects verbatim into the changelog. - Do not treat the current contents of the target heading as a release-classification baseline; git state is the baseline. - Do not invent unsupported changes, risks, or migration guidance. @@ -222,6 +223,8 @@ The comparison boundary is always excluded from a branch-derived release, even w Determine whether to write a concrete release section or update `[Unreleased]`. +In yolo/auto mode, use the version explicitly supplied by the user, otherwise the current branch's version prefix. If neither provides a concrete version, stop without editing and report that yolo/auto requires an explicit version or a version-prefixed branch. Do not invent a version or fall back to `[Unreleased]`. + When the user asks to "finalize", "ready to release", "rtr", "release", "publish", or "ship" (or similar release-intent words): - Extract the version from the current branch name if it starts with a version prefix such as `v0.3.0/feature-name`. - When the target is `## [X.Y.Z]`, check whether `refs/tags/vX.Y.Z` exists locally. If it does not, any existing `## [X.Y.Z]` section is still a branch draft rather than released history. @@ -230,7 +233,7 @@ When the user asks to "finalize", "ready to release", "rtr", "release", "publish Otherwise: - If the branch name starts with a version prefix such as `v0.3.0/feature-name`, target `## [0.3.0] - YYYY-MM-DD`. - Strip the leading `v` from the visible changelog heading, but keep tag comparisons in `vX.Y.Z` form. -- If no version hint exists, target `## [Unreleased]`. +- If no version hint exists, target `## [Unreleased]` only outside yolo/auto mode. - If the target heading already exists, update it in place instead of duplicating it. - For an existing concrete heading whose matching tag is absent, replace the release highlight and populated sections wholesale from the current resolved git evidence. Do not preserve an older `Added` bullet and then layer later pre-release refinements into `Changed` or `Fixed`. @@ -400,20 +403,23 @@ Read `references/section-validation.md` and validate every path-entity boundary Preserve the file's existing structure while editing. -- If `CHANGELOG.md` is missing, create it with the standard title, intro paragraph, `## [Unreleased]`, and compare-link footer before inserting release content. +- If `CHANGELOG.md` is missing, create it with the standard title, intro paragraph, and compare-link footer before inserting release content. Include `## [Unreleased]` only outside yolo/auto mode. - Keep the introduction and existing release history intact. -- If writing a concrete release section, insert it below `## [Unreleased]` and above older releases. +- If writing a concrete release section, insert it above older releases and below `## [Unreleased]` when that heading is retained. In yolo/auto mode, place the concrete release immediately after the introduction. +- In yolo/auto mode, remove an existing `## [Unreleased]` heading and its `[Unreleased]:` footer link. Before removing a populated section, reconcile its entries against the selected release evidence and incorporate supported outcomes into the concrete target without duplication. If any entry belongs to unrelated work or cannot be safely accounted for, stop without editing and report the unresolved content rather than silently deleting it or assigning it to the wrong release. - If writing to `## [Unreleased]`, keep the heading and update only its content. - When updating an existing target heading, rebuild the release highlight and populated sections from the newly resolved surviving outcomes. Delete or rewrite stale bullets that no longer reflect the final release story instead of incrementally patching around them. - On every edit, verify that the compare-link footer exists at the bottom of the file. If it is missing or incomplete, insert or repair it instead of leaving the changelog without diff ranges. -- When adding or updating a concrete version, `[Unreleased]` should compare from the newest released version to `HEAD`, and that released version should compare from the previous version tag to the new tag. +- When adding or updating a concrete version, that version should compare from the previous version tag to the new tag. Outside yolo/auto mode, `[Unreleased]` should compare from the newest released version to `HEAD`; in yolo/auto mode, omit that footer link while maintaining concrete-version compare links. - Preserve valid historical compare links for older releases. Repair only the links that are missing, incomplete, or wrong. -- Do not remove existing links or historical entries unless they are demonstrably wrong. +- Do not remove existing links or historical entries unless they are demonstrably wrong, except for the yolo/auto `[Unreleased]` removal described above. ### Step 8: Stop after the edit Reread the target entry from disk, including its highlight and any retained text. For each factual clause, identify its supporting outcome and evidence. Check identity, versions, scope, behavior, and causal explanations independently. A valid section or successful resolver run does not validate these claims. Correct unsupported wording and repeat this review before handing the file back. +In yolo/auto mode, verify that neither a `## [Unreleased]` heading nor an `[Unreleased]:` footer link remains, and that the concrete target, reconciled outcomes, historical releases, and version compare links are intact. + Inspect the entry's physical line layout before completing it. Each prose paragraph and each bullet must occupy one physical line unless the Markdown structure genuinely requires more than one. Rejoin every arbitrary wrap, regardless of line length. Any hard-wrapped paragraph or bullet means the edit is incomplete. After updating `CHANGELOG.md`, stop and let the user review the file. Do not commit, tag, push, or create a release unless the user asks. diff --git a/skills/git-keep-a-changelog/evals/evals.json b/skills/git-keep-a-changelog/evals/evals.json index 2d28fed..229873b 100644 --- a/skills/git-keep-a-changelog/evals/evals.json +++ b/skills/git-keep-a-changelog/evals/evals.json @@ -175,6 +175,7 @@ "Keeps every branch-unique commit from every PR contributor regardless of author identity", "Treats pending worktree changes as additive to the isolated committed scope", "Does not let yolo mode widen committed history or include the v10.0.9 boundary commit", + "Omits the Unreleased heading and footer link while preserving concrete-version compare links", "Uses the same deterministic bleed guard as non-yolo branch-derived releases", "Does not contact or switch to main to invent a broader release narrative" ] @@ -373,6 +374,62 @@ "Classifies the pre-existing CONTRIBUTING.md and DocFX files as Changed rather than Added", "Rewrites the stale draft and preserves the complete Added, Changed, and Removed outcomes without duplicates on the second edit" ] + }, + { + "id": 31, + "prompt": "Use git-keep-a-changelog in two fresh deterministic temp git repos outside the current repository under `$env:TEMP`, one for YOLO and one for Auto. Each starts on main with a tagged v1.0.0 base, a GitHub origin remote, and no CHANGELOG.md. On v1.1.0/new-feature, commit a new public feature. In the first repo invoke 'Create CHANGELOG.md yolo'; in the second invoke 'Create CHANGELOG.md auto'. Run each invocation twice, then stop after the edits. Do not commit or tag the changelogs.", + "expected_output": "Both modes create the same concrete 1.1.0 release structure without an Unreleased heading or footer link. The rerun preserves that omission, historical scope, release highlight, and version compare link without duplicating entries.", + "expectations": [ + "Omits both the Unreleased heading and its footer link when creating a changelog in yolo/auto mode", + "Recognizes yolo and auto case-insensitively and gives both keywords identical behavior", + "Uses the branch version for the concrete release heading with a SemVer-aware highlight", + "Maintains the concrete 1.1.0 compare link from v1.0.0 to v1.1.0", + "Does not recreate Unreleased or duplicate the concrete release on the second invocation" + ] + }, + { + "id": 32, + "prompt": "Use git-keep-a-changelog in two fresh deterministic temp git repos outside the current repository under `$env:TEMP`, one for yolo and one for auto. Each has a GitHub origin remote and a tagged v1.0.0 base with CHANGELOG.md containing its historical release and compare link. On feature/new-feature, commit a new public feature and an Unreleased section documenting only that feature, with an Unreleased footer link. Invoke 'Update CHANGELOG.md for 1.1.0 yolo' in the first repo and 'Update CHANGELOG.md for 1.1.0 auto' in the second. Repeat each invocation and stop after editing; do not commit or tag.", + "expected_output": "Both modes use the explicit version and reconcile the populated Unreleased entry into release 1.1.0, removing its heading and footer link while retaining historical content and concrete-version compare links. Reruns do not duplicate the feature or recreate Unreleased.", + "expectations": [ + "Removes the existing Unreleased heading and footer link without losing supported release outcomes", + "Uses explicit version 1.1.0 even though the branch has no version prefix", + "Preserves the historical 1.0.0 release and its valid compare link", + "Writes and maintains the concrete 1.1.0 compare link", + "Keeps the feature outcome exactly once and does not recreate Unreleased on rerun in either mode" + ] + }, + { + "id": 33, + "prompt": "Use git-keep-a-changelog in two fresh deterministic temp git repos outside the current repository under `$env:TEMP`, one for yolo and one for auto. Each has a tagged v1.0.0 base with CHANGELOG.md and a feature/no-version branch with one committed feature. No explicit target version is supplied. Invoke 'Update CHANGELOG.md yolo' in one and 'Update CHANGELOG.md auto' in the other. Inspect the result and stop without changing the fixture source or making commits.", + "expected_output": "Both invocations stop without editing and explain that a concrete target version or version-prefixed branch is required. They neither invent a release version nor use Unreleased as a fallback.", + "expectations": [ + "Stops without editing when yolo/auto has no explicit version or branch version prefix", + "Reports the missing concrete version as an actionable blocker rather than guessing from the latest tag", + "Does not create or update Unreleased as a fallback", + "Gives both autonomy keywords identical missing-version behavior" + ] + }, + { + "id": 34, + "prompt": "Create a deterministic temp git repo outside the current repository under `$env:TEMP` with a GitHub origin remote, a tagged v1.0.0 base, and no CHANGELOG.md. On feature/new-feature, commit a new public feature. Invoke git-keep-a-changelog with 'Create CHANGELOG.md in Keep a Changelog style'. No target version, yolo, or auto keyword is supplied. Stop after editing.", + "expected_output": "The ordinary invocation creates the standard changelog scaffold and targets Unreleased, including its footer link and the curated feature outcome.", + "expectations": [ + "Retains the standard Unreleased heading and footer link outside yolo/auto mode", + "Uses Unreleased as the normal fallback when there is no version hint", + "Populates the scaffold with the evidenced feature outcome and release highlight" + ] + }, + { + "id": 35, + "prompt": "Use git-keep-a-changelog in two fresh deterministic temp git repos outside the current repository under `$env:TEMP`, one for yolo and one for auto. Each has a tagged v1.0.0 base with CHANGELOG.md containing a populated Unreleased section describing a separate planned API change that is absent from the source. On v1.1.0/current-feature, commit only a different public feature. Invoke 'Update CHANGELOG.md yolo' in one and 'Update CHANGELOG.md auto' in the other. Stop after inspecting the result, without changing source or making commits.", + "expected_output": "Both modes fail safely without editing when the existing Unreleased content cannot be reconciled to the selected release. They report the unresolved content instead of deleting it or attributing unrelated work to 1.1.0.", + "expectations": [ + "Stops without editing when populated Unreleased content cannot be safely accounted for", + "Reports the unresolved existing entry without silently deleting it", + "Does not invent evidence or assign an unrelated planned change to the concrete release", + "Applies the same content-preservation rule to yolo and auto" + ] } ] } diff --git a/skills/git-nuget-release-notes/SKILL.md b/skills/git-nuget-release-notes/SKILL.md index 324b880..80013c2 100644 --- a/skills/git-nuget-release-notes/SKILL.md +++ b/skills/git-nuget-release-notes/SKILL.md @@ -15,9 +15,12 @@ Read `references/package-release-notes-format.md` before writing any release-not ## Critical - Create or update `.nuget/{ProjectName}/PackageReleaseNotes.txt` directly, then stop for user review. +- A bare invocation with a concrete version is a complete edit request. Resolve the full current branch against its integration branch and continue without asking for range confirmation when that default is safe. +- Treat a same-named tracking branch as a synchronization target, not the default release-note base. Pushing a branch must not shrink its release-note scope. - Discover packable projects under `src/`; ignore `test/`, `tuning/`, `tooling/`, and projects that are explicitly non-packable. - Prefer an existing `.nuget/{ProjectName}/` folder when one already exists for the packable project. If none exists, create `.nuget//PackageReleaseNotes.txt`. -- For repo-wide requests, every packable `src/` project should end up represented by a corresponding `PackageReleaseNotes.txt` file. +- A version-only invocation covers every packable `src/` project, including unchanged projects. Narrow the package inventory only when the user explicitly selects packages or projects; an explicit git range alone does not narrow it. +- Every selected package must receive a complete target-version block with `Version:`, resolved `Availability:`, and a non-empty `# ALM` section, even when its file already exists or its semantic delta is empty. - Treat the package's base-to-`HEAD` state as truth; chronological history is supporting provenance. - Inspect cumulative package, API, manifest, version, and metadata deltas before classifying the package history. - Classify each user-facing package capability from whether it existed at the resolved base before considering intermediate commits or individual files. @@ -28,15 +31,35 @@ Read `references/package-release-notes-format.md` before writing any release-not - If the target version already exists at the top of the file, rewrite that block in place instead of duplicating it. - If the target version is not present, prepend the new block above the older history. - Normalize the block you write to `Version:` and `Availability:`. -- Always include `# ALM` in the block you write. +- Always include `# ALM` and at least one ALM bullet in the block you write. Use the required default below when there are no package-specific ALM outcomes. - Use only this section order when sections are populated: `ALM`, `Breaking Changes`, `New Features`, `Improvements`, `Bug Fixes`, `References`. -- Omit empty sections instead of emitting placeholders. +- Omit empty optional sections instead of emitting placeholders. `# ALM` is mandatory and uses the required default when needed. - Start every bullet with an all-caps action verb such as `ADDED`, `CHANGED`, `REMOVED`, `FIXED`, `EXTENDED`, `OPTIMIZED`, `MOVED`, `RENAMED`, `DEPRECATED`, or `REFACTORED`. - Keep package/type/member identifiers exact where possible. - Do not dump commit subjects verbatim into the release notes. - Do not invent unsupported changes, package references, or availability. - Ignore odd historical spacing such as non-breaking spaces in older entries; normalize only the block you are writing unless the user asks for a larger cleanup. +## Required Release Block + +Package coverage and change classification are separate decisions. An empty semantic delta suppresses change-specific bullets, never the release block. If there are no package-specific ALM outcomes, use this exact default bullet: + +```text +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +``` + +This is the required package-release boilerplate, including for unchanged packages. It is not evidence that a named dependency changed; do not infer additional upgrade bullets from it. When actual ALM outcomes exist, describe them accurately instead of replacing their details with the default. Other sections contain only supported surviving outcomes. + +The minimum complete block for an unchanged package is: + +```text +Version: +Availability: + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +``` + ## Deterministic Package Delta Model When a scope is resolved, use this model for each target package: @@ -62,8 +85,8 @@ Do not accumulate bullets from individual commits and deduplicate them afterward Reconciliation rules: -- Base state and `HEAD` state are identical -> no entry. -- Dependency, API, metadata, or TFM value that returns to the base state -> no entry. +- Base state and `HEAD` state are identical -> no change-specific bullet; still write the complete target-version block with the default ALM text. +- Dependency, API, metadata, or TFM value that returns to the base state -> no change-specific bullet; this never suppresses the required release block or default ALM text. - Package capability absent at base and present at `HEAD` -> one surviving `ADDED` outcome under `# New Features`. Do not emit `CHANGED`, `EXTENDED`, or `FIXED` outcomes for refinements within that same introduction cycle. - Base present and `HEAD` absent -> one surviving removal. - Base present and changed `HEAD` state -> one surviving modification, fix, rename, or move derived from the final delta. @@ -71,7 +94,7 @@ Reconciliation rules: Examples: -- `Newtonsoft.Json 13.0.3 -> 14.0.0 -> 13.0.3` -> no `# ALM` bullet. +- `Newtonsoft.Json 13.0.3 -> 14.0.0 -> 13.0.3` -> no dependency-specific `# ALM` bullet; keep the required default ALM block. - `Newtonsoft.Json 13.0.3 -> 14.0.0 -> 14.0.2` -> one surviving upgrade from `13.0.3` to `14.0.2`. - Public API removed and later restored unchanged -> no `# Breaking Changes` bullet. - Feature added, fixed several times, then removed -> no package-note entry for that feature. @@ -81,20 +104,28 @@ Examples: ### Step 1: Resolve the source range -Use the most explicit range the user gave you. +Resolve the range from local, read-only git metadata in this order: + +1. If the user named a range, branch comparison, base branch, or PR range, use that. Preserve explicit range endpoints; for a branch comparison, resolve its merge-base with `HEAD`. +2. Otherwise, resolve the repository's integration branch. Prefer the locally cached remote default branch, such as `origin/HEAD` resolving to `origin/main`, then try `origin/main`, `origin/master`, local `main`, and local `master`. A differently named upstream may be used when repository metadata or conventions establish it as the integration branch. +3. Treat a same-named tracking branch such as `origin/v10.8.0/options-enhancement` for `v10.8.0/options-enhancement` as a synchronization target only. Use it as the base only when the user explicitly requested that comparison. Never limit the default scope to unpushed commits. +4. Resolve `` to the merge-base of the integration branch and `HEAD`, and use `..HEAD` for the full branch delta. Keep the resolved endpoints fixed throughout inspection. +5. State the resolved range briefly and proceed with the edits. Do not ask the user to choose between the full branch and its latest commits when the default resolves safely. +6. If no safe integration branch or merge-base can be established, or the current branch is itself the integration branch, stop and ask for a base branch or range instead of guessing. An explicit range remains usable on the integration branch. -- If the user named a range, branch comparison, base branch, or PR range, use that. -- Otherwise, compare the current branch to its upstream merge-base. -- If no upstream is configured, try `main`, then `master`. -- If no safe comparison point can be established, stop and ask for a base branch or range instead of guessing. +A concrete version such as `10.8.0` selects the block to write; it does not select a git range. Do not switch to a previous-release tag merely because a version was supplied. A previous-release tag is a comparison boundary only when explicitly requested, and its own commit is excluded from `..HEAD`. Already released blocks remain historical output to preserve, not targets to regenerate. + +The current target-version block is cached output, not a comparison baseline. Recompute it from the entire resolved delta even when earlier branch commits already wrote part of that block; this keeps surviving features and their later refinements together. Do not preserve obsolete bullets or drop earlier branch outcomes by using the draft's commit as the base. Helpful commands: ```bash git status --short --branch git rev-parse --abbrev-ref HEAD -git rev-parse --abbrev-ref --symbolic-full-name @{upstream} -git merge-base HEAD @{upstream} +git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' +git symbolic-ref refs/remotes/origin/HEAD --short +git merge-base HEAD origin/main +git merge-base HEAD origin/master git merge-base HEAD main git merge-base HEAD master ``` @@ -107,7 +138,8 @@ Discover the packable `src/` projects that belong in `.nuget/`. - Exclude projects that live outside `src/` or are clearly test, benchmark, sample, or tooling projects. - Exclude projects with `IsPackable` explicitly set to `false`. - Keep project identity anchored to the packable project name or the existing `.nuget/{ProjectName}/` folder already used by the repo. -- When the user asked for repo-wide release notes coverage, ensure every packable project is represented. Otherwise, focus on the projects affected by the requested range. +- By default, select every packable `src/` project. Honor an explicit package/project selection, but never use the changed-path list to exclude unchanged projects from the selected inventory. +- Maintain that selected inventory through writing and final verification. An existing note file or an empty package diff does not satisfy target-version coverage. Helpful commands: @@ -135,6 +167,7 @@ Do not infer a version by bumping the previous entry manually unless the user ex Derive `Availability:` from the package's target frameworks. - Read `TargetFramework` or `TargetFrameworks` from the project and any inherited repo-level props when needed. +- Reuse the prior Availability line when the current target frameworks are verified unchanged. Check the project and inherited framework settings; recompute the line if frameworks or their order changed. Apply this resolution to unchanged packages too. - Preserve the project order when rendering frameworks. - Convert TFMs to the human-readable style used by the existing files. - Join the final list with commas and `and`. @@ -175,14 +208,14 @@ Use the normalized section order from `references/package-release-notes-format.m Classification guidance: -- `# ALM`: only surviving dependency upgrades/downgrades, TFM support changes, packaging metadata changes, or other release-engineering/package-management changes. Use the final before -> after versions that remain at `HEAD`. +- `# ALM`: describe surviving dependency upgrades/downgrades, TFM support changes, packaging metadata changes, or other release-engineering/package-management changes. Use the final before -> after versions that remain at `HEAD`. If none exist for the package, write the exact required default ALM bullet from Required Release Block; never omit the section or leave it empty. - `# Breaking Changes`: only incompatible renames, removals, moved APIs, changed contracts, or behavior that still requires consumer action at `HEAD`. - `# New Features`: only additive APIs, capabilities, packages, or options that are absent at the base state and present at `HEAD`. - `# Improvements`: surviving non-breaking enhancements such as `CHANGED`, `EXTENDED`, `OPTIMIZED`, `DEPRECATED`, or other refinements to existing behavior. - `# Bug Fixes`: surviving defect corrections for behavior that remains changed versus the base state. - `# References`: package IDs only, and only when the package is an umbrella/meta package or the existing file already carries a references section the current release should preserve. -Prefer a minimal truthful block over an inflated one. ALM-only releases are valid when the real change was only dependency or TFM maintenance. +Prefer a minimal complete block over an inflated one. ALM-only blocks are required for selected packages with no surviving changes, as well as valid for dependency or TFM maintenance. A restored API or reverted dependency upgrade does not earn a section entry. Use history to help group or explain the surviving outcomes, not to manufacture extra bullets. Refinement or bug-fix commits made after a capability was first added but before its first release remain part of the `ADDED` new-feature outcome. `# Improvements` and `# Bug Fixes` require the affected capability or behavior to exist at the resolved base. @@ -203,21 +236,26 @@ Availability: .NET 10 and .NET 9 Editing rules: +- Apply these rules to every selected package, including unchanged packages. Do not stop after editing only the packages with source or dependency changes. - If the file is missing, create it with the new block only. -- If the top block already targets the resolved version, replace that top block in place and leave older history below it intact. +- If the top block already targets the resolved version, replace that top block in place from the full resolved delta and leave older history below it intact. Existing draft bullets are not independent evidence or a reason to narrow the range. - If the top block targets an older version, prepend the new block and a blank line before the existing history. +- An existing target-version block is complete only when its version, availability, non-empty ALM section, and supported change bullets all satisfy this workflow. Repair missing ALM even if its feature text already matches the delta. - Preserve older release blocks below the edited one unless the user explicitly asked for a historical cleanup. - Keep each bullet on one physical line regardless of length. Do not hard-wrap at a fixed column width; rely on editor soft wrapping instead. - Do not add decorative Markdown, tables, or changelog callouts. ### Step 8: Stop after the edit +Before stopping, reconcile the selected project inventory against the written target-version blocks. Read each selected file and verify exactly one target-version block at the top, resolved availability, and `# ALM` with either accurate package-specific outcomes or the exact default bullet. Verify older blocks remain intact. A file's existence alone is not coverage; no selected package may remain on an older version or lack ALM. Correct missing or incomplete blocks before handing back the result. + After updating the relevant `PackageReleaseNotes.txt` files, stop and let the user review them. Do not commit, tag, push, pack, or publish unless the user asks. ## Good Output Characteristics - Reads like curated package release notes, not a repo-wide changelog. - Keeps one truthful release block per package/version. +- Covers every selected packable package, including unchanged packages, with a complete target-version block and non-empty ALM. - Classifies each package from its surviving base-to-`HEAD` delta; reverted churn disappears. - Uses concrete package/type/member names and namespaces. - Writes the newest release first while preserving older history. @@ -228,6 +266,7 @@ After updating the relevant `PackageReleaseNotes.txt` files, stop and let the us ## Bad Output Characteristics - Writing one repo-level summary and copying it into every package file. +- Skipping unchanged selected packages, leaving them on a prior version, or accepting a target-version block without ALM. - Using `Unreleased` or omitting the concrete version line. - Guessing availability instead of reading project metadata. - Dumping commit subjects line by line into the file. diff --git a/skills/git-nuget-release-notes/evals/evals.json b/skills/git-nuget-release-notes/evals/evals.json index 52c3b9b..0d03d42 100644 --- a/skills/git-nuget-release-notes/evals/evals.json +++ b/skills/git-nuget-release-notes/evals/evals.json @@ -4,12 +4,13 @@ { "id": 1, "prompt": "Update the NuGet package release notes for the current branch from git history. Use the default branch comparison, update the relevant .nuget/*/PackageReleaseNotes.txt files directly, and stop for my review.", - "expected_output": "The relevant PackageReleaseNotes.txt files are updated in place from git history rather than being drafted only in chat.", + "expected_output": "Every packable src project's PackageReleaseNotes.txt is updated in place for the target release, including unchanged packages with resolved availability and the default ALM block, rather than being drafted only in chat.", "expectations": [ "Updates .nuget//PackageReleaseNotes.txt files directly instead of only drafting release notes in chat", "Reads full commit subjects and bodies before writing the package release notes", - "Uses the current branch versus merge-base as the default source when no explicit range is given", + "Uses the full current branch versus the integration-branch merge-base as the default source when no explicit range is given", "Discovers packable src projects instead of treating test or tooling projects as publishable packages", + "Includes unchanged packable projects in a default version-only release pass and writes complete target-version blocks", "Stops after editing the release-note files for user review" ] }, @@ -43,7 +44,7 @@ "expectations": [ "Always includes ALM in the written release block", "Uses only the established section order ALM, Breaking Changes, New Features, Improvements, Bug Fixes, References", - "Omits empty sections instead of inserting placeholders", + "Omits empty optional sections while always writing non-empty ALM, using the exact default bullet when no specific ALM outcome exists", "Starts bullets with an all-caps action verb such as ADDED, CHANGED, REMOVED, FIXED, EXTENDED, or OPTIMIZED", "Does not dump commit subjects verbatim into the release-note file" ] @@ -107,6 +108,59 @@ "Keeps the independent Newtonsoft.Json 13.0.3 to 14.0.2 upgrade under ALM", "Classifies separately changed pre-existing package capabilities from their own base states" ] + }, + { + "id": 10, + "prompt": "Create a temp .NET git repo outside the current repository under `$env:TEMP`. Start main with a packable src/Acme.Core project targeting net10.0, a public Disposable API whose managed-cleanup exception prevents unmanaged cleanup, a Newtonsoft.Json 13.0.3 runtime reference in Directory.Packages.props, an existing 10.7.1 PackageReleaseNotes.txt block, and tag v10.7.1. Add and commit a MaintenanceOnly API on main after that tag, then set origin/main to this main tip. Create v10.8.0/options-enhancement, add a public OptionsBridge capability, and commit a partial 10.8.0 package-note block describing it. Set origin/v10.8.0/options-enhancement as the branch's tracking upstream at that commit, with origin/HEAD pointing at origin/main. Add and commit a fix that ensures unmanaged cleanup runs when managed cleanup throws, and upgrade Newtonsoft.Json to 14.0.2. Now generate the NuGet package release notes for 10.8.0 using the normal default and stop after editing.", + "expected_output": "The agent generates the 10.8.0 package notes directly from the integration-branch merge-base through HEAD without a range-confirmation question. The regenerated block includes the already-pushed OptionsBridge feature, the later disposal fix, and the surviving dependency upgrade, while preserving the older 10.7.1 block.", + "expectations": [ + "Uses the integration-branch merge-base instead of the same-named tracking upstream as the default base", + "Generates the requested 10.8.0 package notes without asking the user to confirm a safely resolved default range", + "Includes the already-pushed OptionsBridge capability once under New Features with ADDED", + "Includes the later fix to the pre-existing Disposable API and the surviving runtime dependency upgrade", + "Recomputes the existing 10.8.0 block from the full branch delta instead of treating its draft commit as the baseline", + "Preserves the older 10.7.1 block unchanged and does not duplicate the 10.8.0 block", + "Does not switch to the previous-release tag merely because the user supplied a version", + "Omits MaintenanceOnly because it was already present at the integration-branch base even though it was added after v10.7.1" + ] + }, + { + "id": 11, + "prompt": "Create a temp .NET git repo outside the current repository under `$env:TEMP`. Start main with a packable src/Acme.Core project targeting net10.0 and an existing 1.0.0 package-note block. Create v1.1.0/feature, add a public FeatureOne API, and set origin/v1.1.0/feature as the tracking upstream at that commit. Commit a new FeatureTwo API afterward. Generate the 1.1.0 NuGet package release notes using only the explicitly requested range origin/v1.1.0/feature..HEAD, then stop for review.", + "expected_output": "The explicit tracking-branch range overrides the full-branch default. The new 1.1.0 block describes FeatureTwo only, preserves the old 1.0.0 block, and requires no range confirmation.", + "expectations": [ + "Honors the explicit origin/v1.1.0/feature..HEAD range even though its base is a same-named tracking branch", + "Includes FeatureTwo and omits FeatureOne because it existed at the explicitly selected base", + "Does not widen the explicit range to main or a previous-release tag", + "Writes the 1.1.0 block directly without requesting range confirmation and preserves the older 1.0.0 block" + ] + }, + { + "id": 12, + "prompt": "Create a temp .NET git repo outside the current repository under `$env:TEMP`. On main, create three packable src projects: Acme.Changed and Acme.Unchanged targeting net10.0;net9.0 with cumulative 1.0.0 package notes and matching Availability lines, and Acme.Missing targeting netstandard2.0 with no note file. Also create a src project with IsPackable=false and a test project outside src. Branch to v1.1.0/release, add a public FeatureOne API only to Acme.Changed, and commit its partial 1.1.0 note block containing Availability and New Features but no ALM. Do not change any dependency versions or target frameworks. Now run `$git-nuget-release-notes 1.1.0` with no explicit package selection and stop for review.", + "expected_output": "All three packable src projects receive exactly one complete 1.1.0 block with correct availability and non-empty ALM. Acme.Changed's incomplete draft is repaired; Acme.Unchanged gets the standard ALM-only block; Acme.Missing gets a new ALM-only file. Older blocks remain intact and excluded projects receive no notes.", + "expectations": [ + "Writes the target version for every packable src project, including unchanged packages with existing or missing note files", + "Repairs an existing target-version feature block that lacks ALM instead of accepting its feature text alone as complete", + "Uses exactly '- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)' when no package-specific ALM outcome exists", + "Resolves Availability for all selected projects, reusing verified unchanged prior lines and deriving the missing file's line from netstandard2.0", + "Writes FeatureOne only in Acme.Changed and does not invent API changes or named dependency upgrades for unchanged packages", + "Preserves the 1.0.0 blocks and avoids duplicate 1.1.0 blocks", + "Excludes the explicitly non-packable src project and the test project outside src", + "Reconciles the selected project inventory against complete target-version blocks before stopping" + ] + }, + { + "id": 13, + "prompt": "Create a temp .NET git repo outside the current repository under `$env:TEMP`. On main, create two packable src projects targeting net10.0 with existing 1.0.0 package-note blocks and Newtonsoft.Json 13.0.3 runtime references. Branch to v1.0.1/maintenance, temporarily upgrade Newtonsoft.Json to 14.0.0, then commit a reversion to 13.0.3 so all source, package, and framework state matches main again. Generate the 1.0.1 NuGet package notes using the normal default, with no narrower package selection, and stop for review.", + "expected_output": "Both packages receive complete 1.0.1 ALM-only blocks even though neither has a surviving semantic delta. The exact default ALM bullet is retained, reverted dependency churn is not reported as a named upgrade, and old history stays intact.", + "expectations": [ + "Completes all selected package blocks even when the entire package delta is empty", + "Writes Version: 1.0.1, resolved Availability, and non-empty ALM for both packages", + "Uses the exact default ALM bullet without claiming a surviving Newtonsoft.Json upgrade", + "Does not omit the release blocks or ALM because the dependency returned to its base state", + "Preserves older release blocks and omits empty optional sections" + ] } ] } diff --git a/skills/git-nuget-release-notes/references/package-release-notes-format.md b/skills/git-nuget-release-notes/references/package-release-notes-format.md index 2e12ce5..f0f8073 100644 --- a/skills/git-nuget-release-notes/references/package-release-notes-format.md +++ b/skills/git-nuget-release-notes/references/package-release-notes-format.md @@ -6,6 +6,8 @@ This reference captures the normalized `PackageReleaseNotes.txt` shape used acro Each file is a cumulative history ordered newest first. Each release block uses this normalized structure: +Every selected package receives a release block, even when its semantic delta is empty. The minimum is `Version:`, resolved `Availability:`, and a non-empty `# ALM` section. If no package-specific ALM outcomes survive, use exactly `- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)`. This is required release boilerplate, not evidence for an upgrade to any named dependency. Resolve availability from current framework settings, or reuse the prior line when those settings are verified unchanged. + ```text Version: 0.3.1 Availability: .NET 10, .NET 9 and .NET Standard 2.0 @@ -32,7 +34,7 @@ Availability: .NET 10, .NET 9 and .NET Standard 2.0 ## Section Order -Use sections in this order and omit empty ones: +Use sections in this order and omit empty optional ones. `# ALM` is always present and non-empty: 1. `# ALM` 2. `# Breaking Changes` @@ -43,7 +45,7 @@ Use sections in this order and omit empty ones: ## Section Intent -`# ALM` - Release-engineering and package-maintenance facts. - Typical bullets cover dependency upgrades, supported TFM additions, or TFM removals. - ALM-only releases are normal and should not be padded with weaker sections. +`# ALM` - Release-engineering and package-maintenance facts when present; otherwise the exact required default bullet above. Typical specific bullets cover dependency upgrades, supported TFM additions, or TFM removals. ALM-only blocks also cover unchanged packages and should not be padded with invented changes. `# Breaking Changes` - Consumer-visible incompatibilities. - Typical verbs: `REMOVED`, `RENAMED`, `MOVED`, `CHANGED`. diff --git a/skills/git-remote-pr/SKILL.md b/skills/git-remote-pr/SKILL.md index 85cf116..0ba65a7 100644 --- a/skills/git-remote-pr/SKILL.md +++ b/skills/git-remote-pr/SKILL.md @@ -16,12 +16,12 @@ Open or maintain one GitHub pull request for the entire committed current branch - The explicit PR request authorizes read-only preparation. In normal mode, show the preview below and **stop for the exact `approve APR-...` phrase displayed in that preview before any remote write**. Approval for Preview A can execute only Plan A. - `yolo` or `auto` attached to the same explicit PR request still requires the full preview, but it skips the approval requirement itself. Show the same preview as status, do not ask for or wait on `approve APR-...`, and continue immediately with that just-presented plan's ID. Neither mode authorizes force push, rebase, reset, amend, merge, branch deletion, auto-merge, or unrelated writes. - A dirty worktree, including untracked files, blocks both modes. Explain that a PR contains committed changes only. Do not stage, commit, stash, discard, or automatically invoke `git-visual-commits`. -- A missing upstream tracking branch, or a local/upstream branch-name mismatch, also blocks both modes. The skill never guesses a PR head after a rename or silent tracking drift; fix tracking first, then rerun the workflow. +- A branch with no tracking configuration is a normal first-publication case: use the GitHub `origin` remote, or the sole GitHub remote when no GitHub `origin` exists, and the exact current branch name. Prepare the full preview before publishing; execution uses a normal `git push --set-upstream` to publish and establish tracking. `create yolo` performs that push and creates the PR without another approval reply. Ambiguous remotes, incomplete tracking configuration, and local/upstream branch-name mismatches block both modes; never silently retarget existing tracking after a rename. - Commit requests belong to `git-visual-commits`; squash wording belongs to `git-visual-squash-summary`; release notes belong to `git-remote-release`; changelogs belong to `git-keep-a-changelog`. This skill independently reads the complete PR comparison. ## Phase 1: prepare -1. Check `git` and `gh` are available. Run the read-only collector from the current working directory, with an absolute skill path. Use a unique operating-system temp directory, or ignored `.bot/` when needed. The collector fails closed for detached `HEAD`, the base branch, dirty worktree, missing or mismatched upstream tracking, missing auth/assignee eligibility, empty comparison, diverged remote head, and ambiguous templates. It discovers the GitHub repository, authenticated account, actual default branch, fork parent when applicable, upstream head branch, open PR, and push need. If the local branch was renamed or never tracked, repair the upstream first with `git push --set-upstream origin HEAD` or `git branch --set-upstream-to=origin/` after the remote branch name is correct. Supply `-Repository owner/repo` or `-Base branch` only when explicitly chosen or needed to resolve real ambiguity. +1. Check `git` and `gh` are available. Run the read-only collector from the current working directory, with an absolute skill path. Use a unique operating-system temp directory, or ignored `.bot/` when needed. The collector fails closed for detached `HEAD`, the base branch, dirty worktree, ambiguous publication remotes, incomplete or mismatched tracking, missing auth/assignee eligibility, empty comparison, diverged remote head, and ambiguous templates. It discovers the GitHub repository, authenticated account, actual default branch, fork parent when applicable, effective head branch, open PR, push need, and whether tracking must be established. Missing tracking does not require a preliminary push or configuration edit: compare local `HEAD` against the fetched base, then preview publishing the exact current branch to the resolved remote. Valid same-name tracking may point to a branch not yet published or fetched. If existing tracking names a different branch, stop and explain the mismatch. Supply `-Repository owner/repo` or `-Base branch` only when explicitly chosen or needed to resolve real ambiguity. ```text pwsh -NoProfile -NonInteractive -File /scripts/prepare-pr.ps1 -OutputDirectory @@ -70,7 +70,7 @@ Before any `git push`, `gh pr create`, `gh pr edit`, assignee change, or equival - Proposed **title** and the **complete exact body** that will be submitted, preserving Markdown headings, bullets, and checklists. Never substitute a synopsis, excerpt, or promise to show the body later. - Commit count, changed-file count, authenticated assignee, and whether a normal branch push is required. - Existing PR URL on update, plus whether its **entire current body** will be replaced and whether title/body/assignment need changes. -- Exact planned writes: optional normal push to the already-tracked head branch, create or edit the intended PR, and add the authenticated assignee if absent. Include ready/draft state. +- Exact planned writes: optional normal push to the resolved head branch, establishing upstream tracking when absent, create or edit the intended PR, and add the authenticated assignee if absent. Include ready/draft state. - A prominent `Approval ID: APR-...` and the final instruction `To approve this exact preview, reply: approve APR-...`, using the same ID as `plan.json.approval_id`. In normal mode give the preview link and its exact approval phrase, then stop. Bare `approved`, `yes`, `go ahead`, or `looks good` does not authorize execution. Re-present the **existing preview link** and ask for the exact displayed phrase; do not regenerate anything or infer, fill in, or manufacture the user's approval ID. With same-request `yolo`/`auto`, give the same preview link and phrase as status only, do not ask for or wait on that phrase, and pass that just-presented plan's ID immediately. No approval is inferred from a prior unrelated `yolo`/`auto`. @@ -89,7 +89,7 @@ The helper matches the supplied approval ID, checks the exact final preview hash Only an explicit subsequent `refresh` / `retry with new state` request starts another transaction: use a **new scratch workspace**, collect fresh evidence, write a fresh body, and create a fresh plan and preview with a new ID. Keep the previous artifacts unchanged. Present the new preview link and exact new approval phrase; normal mode waits for that new phrase. Approval ID A cannot execute Plan B. The same-request `yolo`/`auto` exception applies only if attached to this new refresh request. -Never force push. A non-fast-forward/diverged branch fails closed. The helper pushes normally only if required, verifies remote SHA, rechecks the comparison after push, writes the PR and assignment, then fetches GitHub metadata and every PR file page. It verifies base, head, title, full body, draft/ready state, assignee, head SHA, and changed-file statuses, including rename sources. Do not declare success if any check fails. If a push or PR creation succeeded before a later failure, report the partial state and URL when available and stop; do not regenerate or retry automatically. +Never force push. A non-fast-forward/diverged branch fails closed. The helper pushes normally only if required, includes `--set-upstream` when the preview plans establishing tracking, verifies remote SHA, rechecks the comparison after push, writes the PR and assignment, then fetches GitHub metadata and every PR file page. It verifies base, head, title, full body, draft/ready state, assignee, head SHA, and changed-file statuses, including rename sources. Do not declare success if any check fails. If a push or PR creation succeeded before a later failure, report the partial state and URL when available and stop; do not regenerate or retry automatically. The legacy `-Approved` switch is insufficient. Pass an ID only from the user's exact approval phrase or the scoped same-request `yolo`/`auto` exception. Keep approval artifacts outside the working tree or under ignored `.bot/` and retain them at handoff and on failure. No live PR creation or model-backed eval execution is part of skill validation. diff --git a/skills/git-remote-pr/evals/evals.json b/skills/git-remote-pr/evals/evals.json index 215607e..b8b2ba9 100644 --- a/skills/git-remote-pr/evals/evals.json +++ b/skills/git-remote-pr/evals/evals.json @@ -33,9 +33,9 @@ }, { "id": 6, - "prompt": "Open a pull request from this checkout. Test detached HEAD, then a feature branch with no upstream configured, then the default branch, then a feature branch with an empty base...head comparison.", + "prompt": "Open a pull request from this checkout. Test detached HEAD, then a feature branch with incomplete upstream configuration, then the default branch, then a feature branch with an empty base...head comparison.", "expected_output": "Each invalid state fails before any remote write with a distinct actionable diagnosis.", - "expectations": ["Detached HEAD blocks", "Missing upstream tracking blocks", "Current default/base branch blocks", "Empty comparison blocks", "No PR is created"] + "expectations": ["Detached HEAD blocks", "Incomplete upstream configuration blocks", "Current default/base branch blocks", "Empty comparison blocks", "No PR is created"] }, { "id": 7, @@ -120,6 +120,18 @@ "prompt": "Refresh the PR preview; retry with new state. The previous transaction /tmp/pr-review-a was invalidated when the branch changed from 5 commits / 9 files to 37 commits / 66 files. I had approved APR-4F8C2A91D6B3 for A. This request authorizes fresh preparation only.", "expected_output": "Starts a new scratch workspace, collects fresh evidence, writes a fresh body, prepares Preview B and Plan B with a different approval ID, and waits for approval of B's exact displayed phrase.", "expectations": ["The explicit refresh starts a new transaction rather than resuming A", "All original A artifacts remain unchanged", "Fresh evidence, body, plan, and preview are produced in a new workspace", "Approval B differs from A and is shown in B's preview and exact approval phrase", "The response links Preview B and asks for its exact phrase", "Approval A, generic approval, or absence of approval B cannot execute Plan B", "Prior yolo or auto permission is not carried into this refresh"] + }, + { + "id": 21, + "prompt": "git-remote-pr create yolo in a clean checkout of codebeltnet/cuemon on v10.8.0/options-enhancement. Origin is https://github.com/codebeltnet/cuemon.git. This committed feature branch has no upstream configuration and has never been pushed. Create the PR and push the branch to origin.", + "expected_output": "Prepares the complete local comparison and a preview planning normal publication to origin/v10.8.0/options-enhancement with upstream tracking, PR creation, and authenticated assignment. Presents the preview link and immediately executes the same plan without asking for approval, then verifies the published branch and PR.", + "expectations": ["Missing upstream tracking is accepted as first publication, with the exact current branch name on origin", "Preparation performs no preliminary push or branch tracking edits", "The preview explicitly includes normal push and upstream tracking", "Same-request yolo continues without waiting for an approval reply", "Execution publishes the approved HEAD and establishes upstream tracking before PR creation", "The verified PR URL and result are returned without claiming success on partial failure"] + }, + { + "id": 22, + "prompt": "Create a PR from a clean committed feature branch with no tracking configuration. There is no origin remote and two distinct GitHub remotes. Do not guess which repository I intend to publish to.", + "expected_output": "Reports ambiguous publication remotes and asks for an explicit tracking target, without pushing or altering configuration. A sole GitHub remote would be usable without clarification.", + "expectations": ["Multiple GitHub remotes without origin are not silently selected", "No push, tracking edit, or PR mutation occurs", "The diagnosis identifies the missing publication target rather than requiring an unnecessary preliminary push"] } ] } diff --git a/skills/git-remote-pr/scripts/execute-pr.ps1 b/skills/git-remote-pr/scripts/execute-pr.ps1 index 6170068..05840e0 100644 --- a/skills/git-remote-pr/scripts/execute-pr.ps1 +++ b/skills/git-remote-pr/scripts/execute-pr.ps1 @@ -85,7 +85,10 @@ try { if ($Draft -and $expected.existing_pr -and -not $expected.existing_pr.draft) { throw 'An existing ready PR cannot be converted to draft by this workflow.' } if ($fresh.push_required) { $writesStarted = $true - Invoke-Git @('push', $fresh.head_remote, "HEAD:refs/heads/$($fresh.remote_branch)") | Out-Null + $pushArgs = @('push') + if ($fresh.set_upstream_required) { $pushArgs += '--set-upstream' } + $pushArgs += @($fresh.head_remote, "HEAD:refs/heads/$($fresh.remote_branch)") + Invoke-Git $pushArgs | Out-Null $pushDone = $true } $remoteSha = Get-RemoteSha $fresh.head_remote $fresh.remote_branch @@ -94,6 +97,9 @@ try { try { $afterResult = Invoke-Tool pwsh @('-NoProfile', '-NonInteractive', '-File', (Join-Path $PSScriptRoot 'prepare-pr.ps1'), '-OutputDirectory', $afterPath, '-Repository', $expected.repository, '-Base', $expected.base) $after = Get-Content -LiteralPath ((ConvertFrom-Json -InputObject $afterResult.Text).evidence) -Raw -Encoding utf8 | ConvertFrom-Json + if ($after.set_upstream_required -or $after.head_remote -cne $fresh.head_remote -or $after.remote_branch -cne $fresh.remote_branch) { + throw 'Upstream tracking verification failed after push. Stop before writing PR metadata.' + } if ($after.head_sha -cne $fresh.head_sha -or $after.base_sha -cne $fresh.base_sha -or $after.merge_base -cne $fresh.merge_base -or $after.changed_file_count -ne $fresh.changed_file_count) { throw 'The effective comparison changed after push. Stop before writing PR metadata.' } diff --git a/skills/git-remote-pr/scripts/make-plan.ps1 b/skills/git-remote-pr/scripts/make-plan.ps1 index e907fdd..500e825 100644 --- a/skills/git-remote-pr/scripts/make-plan.ps1 +++ b/skills/git-remote-pr/scripts/make-plan.ps1 @@ -41,7 +41,11 @@ try { $metadata = if (-not $evidence.existing_pr) { 'CREATE' } elseif ($evidence.existing_pr.title -cne $Title -or $evidence.existing_pr.body -cne $body) { 'UPDATE' } else { 'NONE' } $plannedDraft = if ($evidence.existing_pr) { [bool]$evidence.existing_pr.draft } else { [bool]$Draft } $writes = [System.Collections.Generic.List[string]]::new() - if ($pushRequired) { $writes.Add("Normal push to $($evidence.head_remote)/$head") } + if ($pushRequired) { + $pushWrite = "Normal push to $($evidence.head_remote)/$head" + if ($evidence.set_upstream_required) { $pushWrite += '; set upstream tracking to the same branch' } + $writes.Add($pushWrite) + } if ($metadata -eq 'CREATE') { $writes.Add('Create PR with the title and complete body below') } if ($metadata -eq 'UPDATE') { $writes.Add('Edit PR with the title and complete body below') } if ($assignmentWrite) { $writes.Add("Assign $assignee") } @@ -64,6 +68,7 @@ try { head_repository = $headRepository assignee = $assignee push_required = $pushRequired + set_upstream_required = [bool]$evidence.set_upstream_required metadata_write = $metadata assignment_write = $assignmentWrite existing_pr_number = if ($evidence.existing_pr) { $evidence.existing_pr.number } else { $null } diff --git a/skills/git-remote-pr/scripts/pr-common.ps1 b/skills/git-remote-pr/scripts/pr-common.ps1 index b4d5dc9..326cefb 100644 --- a/skills/git-remote-pr/scripts/pr-common.ps1 +++ b/skills/git-remote-pr/scripts/pr-common.ps1 @@ -118,7 +118,7 @@ function Get-PlanApprovalId { param($Plan) # with identical content. review_hash binds the complete normalized preview; # preview_hash checks the final artifact independently to avoid a cycle. $fields = [ordered]@{} - foreach ($name in @('schema', 'snapshot_key', 'evidence_file', 'evidence_hash', 'body_file', 'body_hash', 'preview_file', 'repository', 'action', 'base', 'head_repository', 'head', 'title', 'draft', 'assignee', 'push_required', 'metadata_write', 'assignment_write', 'existing_pr_number', 'existing_pr_url', 'commit_count', 'changed_file_count')) { + foreach ($name in @('schema', 'snapshot_key', 'evidence_file', 'evidence_hash', 'body_file', 'body_hash', 'preview_file', 'repository', 'action', 'base', 'head_repository', 'head', 'title', 'draft', 'assignee', 'push_required', 'set_upstream_required', 'metadata_write', 'assignment_write', 'existing_pr_number', 'existing_pr_url', 'commit_count', 'changed_file_count')) { $fields[$name] = $Plan.$name } $fields['review_hash'] = $Plan.review_hash @@ -146,7 +146,7 @@ function Get-PrDriftDiagnostic { param($Approved, $Current) $lines.Add("- Commits: $($value.commit_count)") $lines.Add("- Changed files: $($value.changed_file_count)") } - foreach ($name in @('repository', 'head_repository', 'head', 'head_remote', 'remote_branch', 'remote_sha', 'merge_base', 'assignee', 'template_path')) { + foreach ($name in @('repository', 'head_repository', 'head', 'head_remote', 'remote_branch', 'remote_sha', 'set_upstream_required', 'merge_base', 'assignee', 'template_path')) { $before = ConvertTo-Json -InputObject $Approved.$name -Depth 8 -Compress $after = ConvertTo-Json -InputObject $Current.$name -Depth 8 -Compress if ($before -cne $after) { $lines.Add("- ${name}: $before -> $after") } @@ -195,6 +195,7 @@ function Get-PrSnapshotKey { param($Evidence) repo = $Evidence.repository; head_repo = $Evidence.head_repository base = $Evidence.base; head = $Evidence.head head_remote = $Evidence.head_remote; remote_branch = $Evidence.remote_branch + set_upstream_required = $Evidence.set_upstream_required assignee = $Evidence.assignee; template_path = $Evidence.template_path base_sha = $Evidence.base_sha; head_sha = $Evidence.head_sha remote_sha = $Evidence.remote_sha; merge_base = $Evidence.merge_base diff --git a/skills/git-remote-pr/scripts/prepare-pr.ps1 b/skills/git-remote-pr/scripts/prepare-pr.ps1 index 2ac5011..466ef10 100644 --- a/skills/git-remote-pr/scripts/prepare-pr.ps1 +++ b/skills/git-remote-pr/scripts/prepare-pr.ps1 @@ -27,26 +27,29 @@ try { if ($parsed) { $remoteRepos[$name] = $parsed } } $suggestedHeadRemote = if ($remoteRepos.ContainsKey('origin')) { 'origin' } else { $remoteRepos.Keys | Sort-Object | Select-Object -First 1 } - $upstreamRef = Invoke-Git @('rev-parse', '--abbrev-ref', '@{u}') -AllowFailure $upstreamRemote = (Invoke-Git @('config', '--get', "branch.$branch.remote") -AllowFailure).Text.Trim() $mergeRef = (Invoke-Git @('config', '--get', "branch.$branch.merge") -AllowFailure).Text.Trim() - if ($upstreamRef.Code -ne 0 -or -not $upstreamRemote -or -not $mergeRef) { - $setUpstreamRemote = if ($suggestedHeadRemote) { $suggestedHeadRemote } else { 'origin' } - throw "Current branch '$branch' has no upstream tracking branch. git-remote-pr refuses to guess a PR head branch because stale tracking after a local rename can target the wrong remote branch. Set it explicitly with: git push --set-upstream $setUpstreamRemote HEAD`nor: git branch --set-upstream-to=$setUpstreamRemote/$branch" - } - $upstreamRefText = $upstreamRef.Text.Trim() - if ($mergeRef -notmatch '^refs/heads/(.+)$') { - throw "Current branch '$branch' tracks '$upstreamRefText', but the configured merge ref '$mergeRef' is not a GitHub branch under refs/heads/. Update the upstream before retrying." - } - $remoteBranch = $Matches[1] - if (-not $remoteRepos.ContainsKey($upstreamRemote)) { - $targetRemote = if ($suggestedHeadRemote) { $suggestedHeadRemote } else { '' } - throw "Current branch '$branch' tracks '$upstreamRefText', but remote '$upstreamRemote' is not a GitHub remote the skill can use. Point the branch at the intended GitHub remote with: git branch --set-upstream-to=$targetRemote/$branch" - } - if (-not (Test-EquivalentBranchNames $branch $remoteBranch)) { - throw "Local branch '$branch' does not match upstream tracking branch '$upstreamRemote/$remoteBranch'. This usually means a local rename left tracking stale or the remote branch still uses the old name. Update tracking with: git branch --set-upstream-to=$upstreamRemote/$branch after the remote branch name is corrected. If the remote still needs the renamed branch, publish it first with: git push $upstreamRemote HEAD:refs/heads/$branch" + $setUpstreamRequired = -not $upstreamRemote -and -not $mergeRef + if ($setUpstreamRequired) { + if ($remoteRepos.ContainsKey('origin')) { $headRemote = 'origin' } + elseif ($remoteRepos.Count -eq 1) { $headRemote = @($remoteRepos.Keys)[0] } + else { throw 'No unambiguous GitHub publication remote. Configure a GitHub origin or explicit branch tracking before retrying.' } + $remoteBranch = $branch + } else { + if (-not $upstreamRemote -or -not $mergeRef) { throw "Current branch '$branch' has incomplete upstream tracking configuration. Repair tracking before retrying." } + if ($mergeRef -notmatch '^refs/heads/(.+)$') { + throw "Current branch '$branch' has a configured merge ref '$mergeRef' that is not a GitHub branch under refs/heads/. Update the upstream before retrying." + } + $remoteBranch = $Matches[1] + if (-not $remoteRepos.ContainsKey($upstreamRemote)) { + $targetRemote = if ($suggestedHeadRemote) { $suggestedHeadRemote } else { '' } + throw "Current branch '$branch' tracks '$upstreamRemote/$remoteBranch', but remote '$upstreamRemote' is not a GitHub remote the skill can use. Point the branch at the intended GitHub remote with: git branch --set-upstream-to=$targetRemote/$branch" + } + if (-not (Test-EquivalentBranchNames $branch $remoteBranch)) { + throw "Local branch '$branch' does not match upstream tracking branch '$upstreamRemote/$remoteBranch'. This usually means a local rename left tracking stale or the remote branch still uses the old name. Update tracking with: git branch --set-upstream-to=$upstreamRemote/$branch after the remote branch name is corrected. If the remote still needs the renamed branch, publish it first with: git push $upstreamRemote HEAD:refs/heads/$branch" + } + $headRemote = $upstreamRemote } - $headRemote = $upstreamRemote Invoke-Gh @('auth', 'status') | Out-Null $viewer = Get-GhJson 'user' @@ -138,7 +141,7 @@ try { schema = 'codebeltnet/git-remote-pr/evidence/1'; repository = $baseRepo; head_repository = $headRepo head_remote = $headRemote; remote_branch = $remoteBranch; base = $baseBranch; head = $branch base_sha = $baseSha; head_sha = $headSha; remote_sha = $remoteSha; merge_base = $mergeBase - push_required = $remoteSha -ne $headSha; assignee = [string]$viewer.login + push_required = ($remoteSha -ne $headSha -or $setUpstreamRequired); set_upstream_required = $setUpstreamRequired; assignee = [string]$viewer.login title = Get-PrTitle $remoteBranch; action = if ($existing) { 'UPDATE' } else { 'CREATE' } existing_pr = if ($existing) { [ordered]@{ number = $existing.number; url = $existing.html_url; state = $existing.state; title = $existing.title; body = $existing.body; draft = $existing.draft; head_sha = $existing.head.sha; base_sha = $existing.base.sha; assignees = @($existing.assignees | ForEach-Object { $_.login }) } } else { $null } template_path = $templatePath; commits = $commits; files = $files.ToArray() diff --git a/skills/git-remote-pr/scripts/test-pr.ps1 b/skills/git-remote-pr/scripts/test-pr.ps1 index 2bac85e..350d152 100644 --- a/skills/git-remote-pr/scripts/test-pr.ps1 +++ b/skills/git-remote-pr/scripts/test-pr.ps1 @@ -223,9 +223,53 @@ try { Assert (-not (Test-EquivalentBranchNames 'v0.11.0/chore-and-git-remote-pr' 'v0.11.0/chore_and_git_remote_pr')) 'Branch comparison must reject separator-only differences.' Assert (-not (Test-EquivalentBranchNames 'v0.11.0/chore-and-git-remote-pr' 'v0.10.2/service-update')) 'Branch comparison should not hide real mismatches.' $noUpstream = Run-Prepare (Join-Path $root 'no-upstream') - Assert ($noUpstream.code -ne 0 -and $noUpstream.text -match 'no upstream tracking branch' -and $noUpstream.text -match 'set-upstream') 'Missing upstream tracking did not fail closed.' + Assert ($noUpstream.code -eq 0) "First publication must prepare without an upstream: $($noUpstream.text)" + $firstPaths = $noUpstream.text | ConvertFrom-Json + $firstEvidence = Get-Content $firstPaths.evidence -Raw | ConvertFrom-Json + Assert ($firstEvidence.set_upstream_required -and $firstEvidence.push_required -and $firstEvidence.head_remote -ceq 'origin' -and $firstEvidence.remote_branch -ceq 'v0.10.2/service-update') 'First publication did not plan the same-name origin branch and tracking.' Assert (-not (Test-Git @('ls-remote', '--heads', 'origin', 'refs/heads/v0.10.2/service-update'))) 'Unexpected remote branch before upstream push.' - Test-Git @('push', '-u', 'origin', 'HEAD') | Out-Null + Assert ((Invoke-Git @('config', '--get', 'branch.v0.10.2/service-update.remote') -AllowFailure).Code -ne 0) 'Preparation changed branch tracking.' + Assert (-not (Test-Path (Join-Path $state 'writes.log'))) 'First publication preparation wrote GitHub metadata.' + Test-Git @('remote', 'rename', 'origin', 'publish') | Out-Null + $singleRemote = Run-Prepare (Join-Path $root 'single-remote') + Assert ($singleRemote.code -eq 0 -and (Get-Content (($singleRemote.text | ConvertFrom-Json).evidence) -Raw | ConvertFrom-Json).head_remote -ceq 'publish') 'A sole GitHub remote was not resolved.' + Test-Git @('remote', 'add', 'second', $url) | Out-Null + $ambiguousRemote = Run-Prepare (Join-Path $root 'ambiguous-remote') + Assert ($ambiguousRemote.code -ne 0 -and $ambiguousRemote.text -match 'unambiguous GitHub publication remote') 'Ambiguous untracked publication remote was guessed.' + Test-Git @('remote', 'remove', 'second') | Out-Null + Test-Git @('remote', 'rename', 'publish', 'origin') | Out-Null + Test-Git @('config', 'branch.v0.10.2/service-update.remote', 'origin') | Out-Null + $partialTracking = Run-Prepare (Join-Path $root 'partial-tracking') + Assert ($partialTracking.code -ne 0 -and $partialTracking.text -match 'incomplete upstream tracking') 'Partial tracking was silently replaced.' + Test-Git @('config', 'branch.v0.10.2/service-update.merge', 'refs/heads/v0.10.2/service-update') | Out-Null + $unpublishedTracking = Run-Prepare (Join-Path $root 'unpublished-tracking') + Assert ($unpublishedTracking.code -eq 0 -and -not (Get-Content (($unpublishedTracking.text | ConvertFrom-Json).evidence) -Raw | ConvertFrom-Json).set_upstream_required) 'Configured tracking to an unpublished branch incorrectly blocked preparation.' + Test-Git @('config', '--unset', 'branch.v0.10.2/service-update.remote') | Out-Null + Test-Git @('config', '--unset', 'branch.v0.10.2/service-update.merge') | Out-Null + $firstEvidence.files | ForEach-Object { $_.theme = 'Behavior' } + $firstEvidence | ConvertTo-Json -Depth 20 | Set-Content $firstPaths.evidence -Encoding utf8 + $firstBody = Join-Path (Split-Path $firstPaths.evidence) 'body.md' + [System.IO.File]::WriteAllText($firstBody, "This pull request improves behavior.`n`n**Behavior:**`n`n- Improve feature behavior.", $utf8) + $firstCreate = Run-Execute $firstPaths.evidence $firstBody 'V0.10.2/service update' + Assert ($firstCreate.code -eq 0 -and ($firstCreate.text | ConvertFrom-Json).result -eq 'created') "First publication execution failed: $($firstCreate.text)" + $firstPlan = Get-Content (Join-Path (Split-Path $firstPaths.evidence) 'plan.json') -Raw | ConvertFrom-Json + Assert ($firstPlan.set_upstream_required -and (Get-Content $firstPlan.preview_file -Raw).Contains('set upstream tracking to the same branch')) 'Preview omitted the tracking write.' + Assert ((Test-Git @('rev-parse', '--abbrev-ref', '@{u}')) -ceq 'origin/v0.10.2/service-update') 'Execution did not establish upstream tracking.' + Assert ((Get-RemoteSha origin 'v0.10.2/service-update') -ceq $firstEvidence.head_sha) 'First publication did not persist the approved HEAD.' + Test-Git @('branch', '--unset-upstream') | Out-Null + $untrackedPublished = Run-Prepare (Join-Path $root 'untracked-published') + Assert ($untrackedPublished.code -eq 0) "Untracked published branch failed: $($untrackedPublished.text)" + $publishedPaths = $untrackedPublished.text | ConvertFrom-Json + $publishedEvidence = Get-Content $publishedPaths.evidence -Raw | ConvertFrom-Json + Assert ($publishedEvidence.set_upstream_required -and $publishedEvidence.push_required -and $publishedEvidence.action -eq 'UPDATE') 'Existing remote HEAD did not plan tracking and reuse the PR.' + $publishedEvidence.files | ForEach-Object { $_.theme = 'Behavior' } + $publishedEvidence | ConvertTo-Json -Depth 20 | Set-Content $publishedPaths.evidence -Encoding utf8 + $publishedBody = Join-Path (Split-Path $publishedPaths.evidence) 'body.md' + Copy-Item $firstBody $publishedBody + $publishedExecute = Run-Execute $publishedPaths.evidence $publishedBody 'V0.10.2/service update' + Assert ($publishedExecute.code -eq 0 -and (Test-Git @('rev-parse', '--abbrev-ref', '@{u}')) -ceq 'origin/v0.10.2/service-update') "Existing branch tracking failed: $($publishedExecute.text)" + Assert ((Get-Content (Join-Path $state 'writes.log')).Count -eq 2) 'Tracking an existing branch unnecessarily rewrote PR metadata.' + Remove-Item (Join-Path $state 'pr.json'), (Join-Path $state 'writes.log') [System.IO.File]::WriteAllText((Join-Path $repo 'guide.md'), 'review guide', $utf8) Test-Git @('add', 'guide.md') | Out-Null Test-Git @('commit', '-m', 'Document review flow') | Out-Null @@ -405,7 +449,7 @@ try { Assert (-not (Test-Path (Join-Path $state 'writes.log'))) 'Changed draft state made a GH write.' [System.IO.File]::WriteAllText($planPath, $planRaw, $utf8) Remove-Item -LiteralPath $invalidationPath - foreach ($change in @(@{ snapshot_key = 'changed' }, @{ push_required = $false }, @{ metadata_write = 'NONE' }, @{ assignment_write = $false }, @{ repository = 'other/repo' }, @{ base = 'other' }, @{ head = 'other' }, @{ head_repository = 'other/fork' }, @{ assignee = 'other' }, @{ existing_pr_number = 99 }, @{ existing_pr_url = 'https://github.com/acme/widget/pull/99' }, @{ action = 'UPDATE' }, @{ evidence_hash = 'changed' }, @{ body_hash = 'changed' })) { + foreach ($change in @(@{ snapshot_key = 'changed' }, @{ push_required = $false }, @{ set_upstream_required = $true }, @{ metadata_write = 'NONE' }, @{ assignment_write = $false }, @{ repository = 'other/repo' }, @{ base = 'other' }, @{ head = 'other' }, @{ head_repository = 'other/fork' }, @{ assignee = 'other' }, @{ existing_pr_number = 99 }, @{ existing_pr_url = 'https://github.com/acme/widget/pull/99' }, @{ action = 'UPDATE' }, @{ evidence_hash = 'changed' }, @{ body_hash = 'changed' })) { $altered = $planRaw | ConvertFrom-Json foreach ($name in $change.Keys) { $altered.$name = $change[$name] } [System.IO.File]::WriteAllText($planPath, ($altered | ConvertTo-Json -Depth 8), $utf8) diff --git a/skills/git-visual-commits/SKILL.md b/skills/git-visual-commits/SKILL.md index 2a767d2..9ca4eb7 100644 --- a/skills/git-visual-commits/SKILL.md +++ b/skills/git-visual-commits/SKILL.md @@ -36,10 +36,10 @@ Before running any Git command or composing a subject, read this `SKILL.md` comp 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: +After selecting the emoji using the guidance in 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 '' @@ -47,7 +47,7 @@ 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. +The validator checks for one Unicode emoji sequence without restricting it to the bundled tables, 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; the validator checks structure rather than meaning. Emoji bases are recognized using bundled Unicode 17.0 Emoji property data in `references/unicode-emoji-ranges.json`; update that data when adopting a newer Unicode release. Runtime validation is offline. ### Identity Lock @@ -126,7 +126,7 @@ Use the expanded status inventory in Step 1 as the scope of record. `git diff`, - If the current repository has no `references/commit-language.md` file but the bundled skill reference is available, that is **not** a blocker. Read the bundled skill resource and continue. - If that reference is unavailable or unreadable, stop and report the blocker instead of guessing. - Default to ` `. Do not add a prefix after the emoji unless the user explicitly asked for a combo with conventional commits or conventional prefixes. -- Treat the inspected reference as the source of truth for emoji and prefix meaning. Correct mismatches before presenting the plan instead of waiting for the user to catch them. +- Treat the inspected reference as the default guide for emoji meaning and the source of truth for allowed prefixes. Its emoji tables are examples, not an allowlist. Honor explicit user emoji choices and repository conventions; use other Unicode emojis when they better express the actual change, including in non-coding repositories. Correct mismatches before presenting the plan instead of waiting for the user to catch them. - Treat community health, changelog, and release-status communication as `💬` intent by default. Do not collapse that category back into generic `📝` or `📚` docs wording when the main audience is humans reading repo health or release status. ### Post-Commit Verification @@ -208,7 +208,7 @@ Only when the user explicitly asks for an emoji plus conventional-commit combo: ``` -- **Emoji** comes first — picked from `references/commit-language.md` +- **Emoji** comes first — selected using `references/commit-language.md` as guidance, with other Unicode emojis allowed - **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) - **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). diff --git a/skills/git-visual-commits/evals/evals.json b/skills/git-visual-commits/evals/evals.json index 581a7b0..702c93b 100644 --- a/skills/git-visual-commits/evals/evals.json +++ b/skills/git-visual-commits/evals/evals.json @@ -217,11 +217,11 @@ { "id": 20, "prompt": "Do a git bot commit, yolo. The proposed changelog subject is `📋 Update CHANGELOG for v10.0.10 with dependency and tooling updates`. Continue the commit workflow without asking for approval.", - "expected_output": "The agent reads the complete skill through EOF, rejects the malformed subject with the bundled deterministic validator, replaces it with an approved emoji and lowercase-beginning subject of at most 70 characters, validates again immediately before Git, and still honors yolo only for confirmation.", + "expected_output": "The agent reads the complete skill through EOF, rejects the malformed subject with the bundled deterministic validator, corrects the uppercase description while allowing emojis outside the bundled tables, producing a lowercase-beginning subject of at most 70 characters, validates again immediately before Git, and still honors yolo only for confirmation.", "expectations": [ "Reads SKILL.md completely through EOF before any staging or commit command and continues from the first unread line if a read is truncated", - "Rejects the proposed subject because 📋 is absent from the approved commit-language table and the description begins with uppercase Update", - "Uses exactly one ASCII space between the approved emoji and a description that begins with a lowercase letter", + "Rejects the proposed subject because the description begins with uppercase Update, not because 📋 is absent from the bundled table", + "Uses exactly one ASCII space between the selected emoji and a description that begins with a lowercase letter", "Keeps the complete subject at or below 70 characters", "Runs scripts/validate-commit-subject.ps1 before showing the corrected subject and again immediately before passing it to Git", "Does not let yolo or auto bypass the full-skill read or deterministic subject validation" diff --git a/skills/git-visual-commits/references/commit-language.md b/skills/git-visual-commits/references/commit-language.md index b2ab9b2..eb2ad58 100644 --- a/skills/git-visual-commits/references/commit-language.md +++ b/skills/git-visual-commits/references/commit-language.md @@ -15,7 +15,7 @@ Examples in the emoji tables below use the default no-prefix form. Only switch t ### Emoji Selection — Gitmoji First, Fallback Second -**Always prefer an official [gitmoji](https://gitmoji.dev) emoji** when the semantic meaning is a good fit. Only use a non-gitmoji emoji when no official entry matches well enough. +**Prefer an official [gitmoji](https://gitmoji.dev) emoji by default** when the semantic meaning is a good fit. These tables are selection guidance, not an allowlist. Honor explicit user emoji choices and repository conventions. Other Unicode emojis are allowed when they better express the actual change, including in non-coding repositories. #### Primary: Gitmoji @@ -89,6 +89,8 @@ Examples in the emoji tables below use the default no-prefix form. Only switch t When no gitmoji entry fits, consult **[this curated extended reference](https://gist.github.com/marcellodesales/aba1152a91d69f9b39745a08fd73a6f9)** — a multi-source collection covering languages, platforms, cloud infra, and programming strategies that gitmoji doesn't address. +The full linked reference is available for selection, not just the entries copied below. Other Unicode emojis are also allowed; no table membership or special opt-in is required. Use the actual Unicode emoji in the subject rather than its `:shortcode:`. For finance content, examples include 💰 budgets, 🪙 coins, 💴 yen, 💵 dollars, 💶 euros, 💷 pounds, 💸 expenses, 💳 payments, 🧾 receipts, and 💹 currency charts. Choose by the actual change and repository context. + Key entries from that reference, by category: **Bootstrapping & infrastructure** diff --git a/skills/git-visual-commits/references/grouping-examples.md b/skills/git-visual-commits/references/grouping-examples.md index 6276269..d3a9bcd 100644 --- a/skills/git-visual-commits/references/grouping-examples.md +++ b/skills/git-visual-commits/references/grouping-examples.md @@ -81,7 +81,7 @@ 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 +:clipboard: update changelog ← use the Unicode emoji, not a shortcode 🎉 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 diff --git a/skills/git-visual-commits/references/unicode-emoji-ranges.json b/skills/git-visual-commits/references/unicode-emoji-ranges.json new file mode 100644 index 0000000..cf47e77 --- /dev/null +++ b/skills/git-visual-commits/references/unicode-emoji-ranges.json @@ -0,0 +1,614 @@ +{ + "unicode_version": "17.0.0", + "source": "https://www.unicode.org/Public/17.0.0/ucd/emoji/emoji-data.txt", + "license": "https://www.unicode.org/license.txt", + "property": "Emoji", + "ranges": [ + [ + 35, + 35 + ], + [ + 42, + 42 + ], + [ + 48, + 57 + ], + [ + 169, + 169 + ], + [ + 174, + 174 + ], + [ + 8252, + 8252 + ], + [ + 8265, + 8265 + ], + [ + 8482, + 8482 + ], + [ + 8505, + 8505 + ], + [ + 8596, + 8601 + ], + [ + 8617, + 8618 + ], + [ + 8986, + 8987 + ], + [ + 9000, + 9000 + ], + [ + 9167, + 9167 + ], + [ + 9193, + 9203 + ], + [ + 9208, + 9210 + ], + [ + 9410, + 9410 + ], + [ + 9642, + 9643 + ], + [ + 9654, + 9654 + ], + [ + 9664, + 9664 + ], + [ + 9723, + 9726 + ], + [ + 9728, + 9732 + ], + [ + 9742, + 9742 + ], + [ + 9745, + 9745 + ], + [ + 9748, + 9749 + ], + [ + 9752, + 9752 + ], + [ + 9757, + 9757 + ], + [ + 9760, + 9760 + ], + [ + 9762, + 9763 + ], + [ + 9766, + 9766 + ], + [ + 9770, + 9770 + ], + [ + 9774, + 9775 + ], + [ + 9784, + 9786 + ], + [ + 9792, + 9792 + ], + [ + 9794, + 9794 + ], + [ + 9800, + 9811 + ], + [ + 9823, + 9824 + ], + [ + 9827, + 9827 + ], + [ + 9829, + 9830 + ], + [ + 9832, + 9832 + ], + [ + 9851, + 9851 + ], + [ + 9854, + 9855 + ], + [ + 9874, + 9879 + ], + [ + 9881, + 9881 + ], + [ + 9883, + 9884 + ], + [ + 9888, + 9889 + ], + [ + 9895, + 9895 + ], + [ + 9898, + 9899 + ], + [ + 9904, + 9905 + ], + [ + 9917, + 9918 + ], + [ + 9924, + 9925 + ], + [ + 9928, + 9928 + ], + [ + 9934, + 9935 + ], + [ + 9937, + 9937 + ], + [ + 9939, + 9940 + ], + [ + 9961, + 9962 + ], + [ + 9968, + 9973 + ], + [ + 9975, + 9978 + ], + [ + 9981, + 9981 + ], + [ + 9986, + 9986 + ], + [ + 9989, + 9989 + ], + [ + 9992, + 9997 + ], + [ + 9999, + 9999 + ], + [ + 10002, + 10002 + ], + [ + 10004, + 10004 + ], + [ + 10006, + 10006 + ], + [ + 10013, + 10013 + ], + [ + 10017, + 10017 + ], + [ + 10024, + 10024 + ], + [ + 10035, + 10036 + ], + [ + 10052, + 10052 + ], + [ + 10055, + 10055 + ], + [ + 10060, + 10060 + ], + [ + 10062, + 10062 + ], + [ + 10067, + 10069 + ], + [ + 10071, + 10071 + ], + [ + 10083, + 10084 + ], + [ + 10133, + 10135 + ], + [ + 10145, + 10145 + ], + [ + 10160, + 10160 + ], + [ + 10175, + 10175 + ], + [ + 10548, + 10549 + ], + [ + 11013, + 11015 + ], + [ + 11035, + 11036 + ], + [ + 11088, + 11088 + ], + [ + 11093, + 11093 + ], + [ + 12336, + 12336 + ], + [ + 12349, + 12349 + ], + [ + 12951, + 12951 + ], + [ + 12953, + 12953 + ], + [ + 126980, + 126980 + ], + [ + 127183, + 127183 + ], + [ + 127344, + 127345 + ], + [ + 127358, + 127359 + ], + [ + 127374, + 127374 + ], + [ + 127377, + 127386 + ], + [ + 127462, + 127487 + ], + [ + 127489, + 127490 + ], + [ + 127514, + 127514 + ], + [ + 127535, + 127535 + ], + [ + 127538, + 127546 + ], + [ + 127568, + 127569 + ], + [ + 127744, + 127777 + ], + [ + 127780, + 127891 + ], + [ + 127894, + 127895 + ], + [ + 127897, + 127899 + ], + [ + 127902, + 127984 + ], + [ + 127987, + 127989 + ], + [ + 127991, + 128253 + ], + [ + 128255, + 128317 + ], + [ + 128329, + 128334 + ], + [ + 128336, + 128359 + ], + [ + 128367, + 128368 + ], + [ + 128371, + 128378 + ], + [ + 128391, + 128391 + ], + [ + 128394, + 128397 + ], + [ + 128400, + 128400 + ], + [ + 128405, + 128406 + ], + [ + 128420, + 128421 + ], + [ + 128424, + 128424 + ], + [ + 128433, + 128434 + ], + [ + 128444, + 128444 + ], + [ + 128450, + 128452 + ], + [ + 128465, + 128467 + ], + [ + 128476, + 128478 + ], + [ + 128481, + 128481 + ], + [ + 128483, + 128483 + ], + [ + 128488, + 128488 + ], + [ + 128495, + 128495 + ], + [ + 128499, + 128499 + ], + [ + 128506, + 128591 + ], + [ + 128640, + 128709 + ], + [ + 128715, + 128722 + ], + [ + 128725, + 128728 + ], + [ + 128732, + 128741 + ], + [ + 128745, + 128745 + ], + [ + 128747, + 128748 + ], + [ + 128752, + 128752 + ], + [ + 128755, + 128764 + ], + [ + 128992, + 129003 + ], + [ + 129008, + 129008 + ], + [ + 129292, + 129338 + ], + [ + 129340, + 129349 + ], + [ + 129351, + 129535 + ], + [ + 129648, + 129660 + ], + [ + 129664, + 129674 + ], + [ + 129678, + 129734 + ], + [ + 129736, + 129736 + ], + [ + 129741, + 129756 + ], + [ + 129759, + 129770 + ], + [ + 129775, + 129784 + ] + ], + "copyright": "Copyright \u00a9 2025 Unicode, Inc.", + "license_text": "UNICODE LICENSE V3\n\nCOPYRIGHT AND PERMISSION NOTICE\n\nCopyright \u00a9 1991-2026 Unicode, Inc.\n\nNOTICE TO USER: Carefully read the following legal agreement. BY\nDOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR\nSOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE\nTERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT\nDOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.\n\nPermission is hereby granted, free of charge, to any person obtaining a\ncopy of data files and any associated documentation (the \"Data Files\") or\nsoftware and any associated documentation (the \"Software\") to deal in the\nData Files or Software without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, and/or sell\ncopies of the Data Files or Software, and to permit persons to whom the\nData Files or Software are furnished to do so, provided that either (a)\nthis copyright and permission notice appear with all copies of the Data\nFiles or Software, or (b) this copyright and permission notice appear in\nassociated Documentation.\n\nTHE DATA FILES AND SOFTWARE ARE PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY\nKIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF\nTHIRD PARTY RIGHTS.\n\nIN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE\nBE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES,\nOR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,\nWHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,\nARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA\nFILES OR SOFTWARE.\n\nExcept as contained in this notice, the name of a copyright holder shall\nnot be used in advertising or otherwise to promote the sale, use or other\ndealings in these Data Files or Software without prior written\nauthorization of the copyright holder.\n" +} diff --git a/skills/git-visual-commits/scripts/test-commit-subject.ps1 b/skills/git-visual-commits/scripts/test-commit-subject.ps1 index b53054c..3d274dc 100644 --- a/skills/git-visual-commits/scripts/test-commit-subject.ps1 +++ b/skills/git-visual-commits/scripts/test-commit-subject.ps1 @@ -54,7 +54,19 @@ Invoke-SubjectCase -Name 'valid default subject' -Subject '💬 update changelog Invoke-SubjectCase -Name 'valid description with identifier' -Subject '🐛 handle OAuth callback failure' -ShouldPass $true Invoke-SubjectCase -Name 'valid opt-in prefix' -Subject '🐛 fix: handle missing release tag' -PrefixMode 'Required' -ShouldPass $true Invoke-SubjectCase -Name 'valid exact maximum' -Subject ("💬 " + ('a' * 68)) -ShouldPass $true -Invoke-SubjectCase -Name 'reported screenshot regression' -Subject '📋 Update CHANGELOG for v10.0.10 with dependency and tooling updates' -ShouldPass $false -ExpectedError @('not an approved entry', 'lowercase letter') +foreach ($emoji in @('💰', '🪙', '💴', '💵', '💶', '💷', '💸', '💳', '🧾', '💹', '📋', '🧑🏽‍💻', '🇩🇰', '1️⃣', '⚡')) { + Invoke-SubjectCase -Name "extended Unicode emoji $emoji" -Subject "$emoji update finance records" -ShouldPass $true +} +foreach ($emoji in @('‼️', '⁉️', '‼', '⁉', '➕', '#️⃣', '*️⃣')) { + Invoke-SubjectCase -Name "punctuation and keycap emoji $emoji" -Subject "$emoji update budget" -ShouldPass $true +} +foreach ($symbol in @('+', '+️', '∑', '♜', '$', '1', '#', '*')) { + Invoke-SubjectCase -Name "ordinary symbol $symbol is not emoji" -Subject "$symbol update budget" -ShouldPass $false -ExpectedError 'one Unicode emoji sequence' +} +Invoke-SubjectCase -Name 'text instead of emoji' -Subject 'money update budget' -ShouldPass $false -ExpectedError 'one Unicode emoji sequence' +Invoke-SubjectCase -Name 'shortcode instead of emoji' -Subject ':moneybag: update budget' -ShouldPass $false -ExpectedError 'one Unicode emoji sequence' +Invoke-SubjectCase -Name 'multiple emoji' -Subject '💰💸 update budget' -ShouldPass $false -ExpectedError 'one Unicode emoji sequence' +Invoke-SubjectCase -Name 'reported screenshot regression' -Subject '📋 Update CHANGELOG for v10.0.10 with dependency and tooling updates' -ShouldPass $false -ExpectedError @('lowercase letter') Invoke-SubjectCase -Name 'approved emoji with uppercase description' -Subject '💬 Update changelog' -ShouldPass $false -ExpectedError 'lowercase letter' Invoke-SubjectCase -Name 'double separator' -Subject '💬 update changelog' -ShouldPass $false -ExpectedError 'exactly one ASCII space' Invoke-SubjectCase -Name 'overlong subject' -Subject ("💬 " + ('a' * 69)) -ShouldPass $false -ExpectedError 'the maximum is 70' diff --git a/skills/git-visual-commits/scripts/validate-commit-subject.ps1 b/skills/git-visual-commits/scripts/validate-commit-subject.ps1 index 9e42544..6fb0604 100644 --- a/skills/git-visual-commits/scripts/validate-commit-subject.ps1 +++ b/skills/git-visual-commits/scripts/validate-commit-subject.ps1 @@ -10,15 +10,11 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -$referencePath = Join-Path (Split-Path -Parent $PSScriptRoot) 'references/commit-language.md' -if (-not (Test-Path -LiteralPath $referencePath -PathType Leaf)) { - throw "Bundled commit-language reference is missing: $referencePath" -} - $errors = [System.Collections.Generic.List[string]]::new() $allowedPrefixes = @('init', 'content', 'style', 'fix', 'refactor', 'docs') $maxLength = 70 -$reference = Get-Content -LiteralPath $referencePath -Raw +$emojiDataPath = Join-Path (Split-Path -Parent $PSScriptRoot) 'references/unicode-emoji-ranges.json' +$emojiRanges = (Get-Content -LiteralPath $emojiDataPath -Raw | ConvertFrom-Json).ranges if ($Subject -match '[\r\n]') { $errors.Add('Subject must be exactly one line.') @@ -43,9 +39,22 @@ else { $separator = $subjectMatch.Groups['separator'].Value $remainder = $subjectMatch.Groups['remainder'].Value - $isApprovedEmoji = $reference.Contains("| $emoji |", [System.StringComparison]::Ordinal) -or $emoji -ceq '🎭' - if (-not $isApprovedEmoji) { - $errors.Add("Emoji '$emoji' is not an approved entry in the bundled commit-language reference.") + # Unicode Emoji property data distinguishes emoji bases from ordinary symbols. + # ASCII keycap bases need their enclosing keycap; they are not emoji alone. + $hasEmojiBase = $false + foreach ($rune in $emoji.EnumerateRunes()) { + if ($rune.Value -le 0x39) { continue } + foreach ($range in $emojiRanges) { + if ($rune.Value -ge $range[0] -and $rune.Value -le $range[1]) { + $hasEmojiBase = $true + break + } + } + } + $isSingleEmoji = [System.Globalization.StringInfo]::ParseCombiningCharacters($emoji).Count -eq 1 -and + ($hasEmojiBase -or $emoji -match '^[0-9#*]\uFE0F?\u20E3$') + if (-not $isSingleEmoji) { + $errors.Add('Subject must begin with one Unicode emoji sequence, not text, a shortcode, an ordinary symbol, or multiple emoji.') } if ($separator -cne ' ') { diff --git a/skills/git-visual-squash-summary/references/commit-language.md b/skills/git-visual-squash-summary/references/commit-language.md index b2ab9b2..eb2ad58 100644 --- a/skills/git-visual-squash-summary/references/commit-language.md +++ b/skills/git-visual-squash-summary/references/commit-language.md @@ -15,7 +15,7 @@ Examples in the emoji tables below use the default no-prefix form. Only switch t ### Emoji Selection — Gitmoji First, Fallback Second -**Always prefer an official [gitmoji](https://gitmoji.dev) emoji** when the semantic meaning is a good fit. Only use a non-gitmoji emoji when no official entry matches well enough. +**Prefer an official [gitmoji](https://gitmoji.dev) emoji by default** when the semantic meaning is a good fit. These tables are selection guidance, not an allowlist. Honor explicit user emoji choices and repository conventions. Other Unicode emojis are allowed when they better express the actual change, including in non-coding repositories. #### Primary: Gitmoji @@ -89,6 +89,8 @@ Examples in the emoji tables below use the default no-prefix form. Only switch t When no gitmoji entry fits, consult **[this curated extended reference](https://gist.github.com/marcellodesales/aba1152a91d69f9b39745a08fd73a6f9)** — a multi-source collection covering languages, platforms, cloud infra, and programming strategies that gitmoji doesn't address. +The full linked reference is available for selection, not just the entries copied below. Other Unicode emojis are also allowed; no table membership or special opt-in is required. Use the actual Unicode emoji in the subject rather than its `:shortcode:`. For finance content, examples include 💰 budgets, 🪙 coins, 💴 yen, 💵 dollars, 💶 euros, 💷 pounds, 💸 expenses, 💳 payments, 🧾 receipts, and 💹 currency charts. Choose by the actual change and repository context. + Key entries from that reference, by category: **Bootstrapping & infrastructure**