diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 34d44ff..3662ee8 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "claudius", - "version": "4.3.0", + "version": "4.4.0", "description": "Collection of specialized development agents and skills for Claude Code", "author": { "name": "lklimek", diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d5a11c..af8c378 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). This project use ## [Unreleased] +## [4.4.0] - 2026-05-29 + +### Changed + +- PR bodies now lead with a "Why this PR exists" rationale section (problem, reproduction/threat scenario, blocking relationship) before What/Testing/Breaking/Checklist. The skeleton lives in `skills/git-and-github/SKILL.md` (§Creating a PR); `skills/push/SKILL.md` delegates to it. Pinned by `tests/test_pr_body_template.py`. + ## [4.3.0] - 2026-05-29 ### Added diff --git a/skills/git-and-github/SKILL.md b/skills/git-and-github/SKILL.md index 0b754c1..6db76ee 100644 --- a/skills/git-and-github/SKILL.md +++ b/skills/git-and-github/SKILL.md @@ -61,7 +61,22 @@ If a push fails with 403 or "Resource not accessible" and `ghsudo` is installed, Check for a PR template first. If a template exists, read and fill it in. When applicable, include an informal user story (what the user can achieve, no technical details -- start with "Imagine you are..."). -Always create PRs as drafts. +The PR body **must lead with a `## Why this PR exists` section** — reviewers read motivation before mechanics. Use this skeleton (drop empty sections): + +```markdown +## Why this PR exists +- **Problem**: 1-2 plain-language sentences on what's broken or missing. +- **What breaks without it**: a concrete reproduction or threat scenario — numbered steps or a short narrative showing the actual failure/misbehaviour, not an abstract claim. +- **Blocking relationship**: prerequisite for / depends on / stacked atop PR #N, if any. + +## What was done +## Testing +## Breaking changes +## Checklist +## Attribution +``` + +`Why this PR exists` comes first; the remaining sections follow in that order. Always create PRs as drafts. ### Reviewing a PR diff --git a/skills/push/SKILL.md b/skills/push/SKILL.md index 5832f85..7618dba 100644 --- a/skills/push/SKILL.md +++ b/skills/push/SKILL.md @@ -28,6 +28,7 @@ Load `claudius:git-and-github` skill first — all commit, push, PR, and attribu 4. **Push** to remote 5. **PR** + - PR body MUST lead with a "Why this PR exists" section per `git-and-github` §Creating a PR - If PR exists for this branch: update its title and description to reflect current changes - If no PR: create a draft PR with summary + test plan per `git-and-github` diff --git a/tests/test_pr_body_template.py b/tests/test_pr_body_template.py new file mode 100644 index 0000000..319ba3b --- /dev/null +++ b/tests/test_pr_body_template.py @@ -0,0 +1,58 @@ +"""Regression guard: the canonical PR-body template must lead with "Why this PR exists". + +PR descriptions are owned by ONE skill (`git-and-github`); `push` delegates to it. +This test pins the contract so a refactor can't silently demote the rationale section +below the mechanics: `git-and-github/SKILL.md` must define a "Why this PR exists" +heading positioned AHEAD of the "What was done"/"Testing" sections, and `push/SKILL.md` +must reference it rather than inlining a duplicate template. +""" + +from __future__ import annotations + +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +GIT_GITHUB = REPO_ROOT / "skills" / "git-and-github" / "SKILL.md" +PUSH = REPO_ROOT / "skills" / "push" / "SKILL.md" + +WHY = "Why this PR exists" + + +def test_git_github_has_why_section() -> None: + text = GIT_GITHUB.read_text(encoding="utf-8") + assert ( + f"## {WHY}" in text + ), f"{GIT_GITHUB}: missing '## {WHY}' heading in PR-body template" + + +def test_why_section_leads_what_and_testing() -> None: + text = GIT_GITHUB.read_text(encoding="utf-8") + why = text.index(f"## {WHY}") + what = text.index("## What was done") + testing = text.index("## Testing") + assert why < what, f"{GIT_GITHUB}: '{WHY}' must precede 'What was done'" + assert why < testing, f"{GIT_GITHUB}: '{WHY}' must precede 'Testing'" + + +def test_why_section_demands_reproduction_and_blocking() -> None: + """The skeleton must prompt for a concrete repro/threat scenario and blocking relationship.""" + text = GIT_GITHUB.read_text(encoding="utf-8") + lowered = text.lower() + assert ( + "reproduction" in lowered or "threat scenario" in lowered + ), f"{GIT_GITHUB}: '{WHY}' skeleton must ask for a reproduction or threat scenario" + assert ( + "blocking" in lowered + ), f"{GIT_GITHUB}: '{WHY}' skeleton must ask for the blocking relationship" + + +def test_push_references_why_without_inlining_template() -> None: + text = PUSH.read_text(encoding="utf-8") + assert WHY in text, f"{PUSH}: must reference the '{WHY}' section" + assert ( + "git-and-github" in text + ), f"{PUSH}: must delegate to git-and-github for the template" + # No duplicated skeleton: push references the section, it doesn't redefine the heading. + assert ( + f"## {WHY}" not in text + ), f"{PUSH}: must not inline a duplicate '## {WHY}' template — delegate to git-and-github"