diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 64d7ec3..097335a 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "claudius", - "version": "5.13.0", + "version": "5.14.0", "description": "Collection of specialized development agents and skills for Claude Code", "author": { "name": "lklimek", diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d47e18..6393b1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ Format follows [Keep a Changelog](https://keepachangelog.com/). This project use ## [Unreleased] +## [5.14.0] - 2026-07-21 + +### Changed + +- **PR-body template** (`skills/git-and-github/SKILL.md` §Creating a PR): replaced the "Why this PR exists" skeleton with a plain-language-first structure — `TL;DR` (one sentence) -> `## User story` ("As a **\**, I want to \, to achieve \.") -> `## Scenario` (`### Base flow` / `### Actual behavior` / `### Expected behavior`) -> `## Detailed discussion` (nests the former `What was done`/`Testing`/`Breaking changes`/`Checklist`/`Attribution` sub-sections, plus problem statement and blocking relationship). The first three sections must stay free of specialized terms and code identifiers — aimed at a technical product manager or external reviewer; `Detailed discussion` is the technical section for implementors and AI agents. +- **Issue-body template** (`skills/git-and-github/SKILL.md` §Issues): unified onto the same `TL;DR` -> `User story` -> `Scenario` -> `Detailed discussion` shape as PRs, replacing the old "feature/enhancement issues need a `### User Story`" bullet. For bug reports, `Scenario` doubles as the reproduction; feature requests with nothing to reproduce may drop it. +- **Both templates externalized**: the literal skeletons moved out of `SKILL.md` prose into `skills/git-and-github/references/pr-body-template.md` and `references/issue-body-template.md`; `SKILL.md` and `references/gh-cli-fallback.md` now link to them instead of inlining a copy (the `gh` CLI fallback now fills in the template file and passes it via `--body-file`); `push/SKILL.md` continues to delegate to `git-and-github` without inlining its own copy. Pinned by `tests/test_pr_body_template.py` and the new `tests/test_issue_body_template.py`. +- Both templates gained a `### Prior work` section (links to possibly related/similar PRs or issues, each with a one-sentence summary of how it relates to the one being filed), placed between `Checklist`/`Detailed discussion` and `Attribution` so the mandatory attribution footer stays the true last section, per Copilot review on #76. Same review flagged the template pointer sentences (`SKILL.md`, `gh-cli-fallback.md`) as ambiguous about copying the whole template file vs. just its fenced skeleton — both templates and all four pointer sentences now say explicitly to copy only the fenced block. + ## [5.13.0] - 2026-07-20 ### Added diff --git a/skills/git-and-github/SKILL.md b/skills/git-and-github/SKILL.md index 4c75db7..04bd445 100644 --- a/skills/git-and-github/SKILL.md +++ b/skills/git-and-github/SKILL.md @@ -57,26 +57,17 @@ Always ask explicit confirmation before every push, even if the user agreed earl ### Creating a PR -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..."). +Check for a PR template first. If a template exists, read and fill it in, folding its required content into the skeleton linked below rather than replacing it. -The PR body **must lead with a `## Why this PR exists` section** — reviewers read motivation before mechanics. Use this skeleton (drop empty sections): +The PR body **must lead with a plain-language summary before any implementation detail** — a technical product manager or an external reviewer with no code context must understand everything before `Detailed discussion` at a glance. Copy the skeleton from [pr-body-template.md](references/pr-body-template.md) (the fenced block only, not the page's title or explanatory text) and fill it in. -```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. +**`TL;DR` / `User story` / `Scenario` are user-facing** — plain language only, no specialized terms, no internal implementation details, no code identifiers. Describe strictly user-observable behavior (for an API/CLI, the calling developer *is* the user). `User story` follows the same "As a ``..." shape as the Issues `User story` below, phrased for a change already made. `Scenario` isn't only for bugs: for a new feature, `Actual behavior` is what's missing/impossible today and `Expected behavior` is what becomes possible after this PR — no failure or "actual" bug is required. For a pure internal change with no user-observable effect, drop `User story` and `Scenario` entirely and say so in `Detailed discussion`. Note any blocking relationship (prerequisite for / depends on / stacked atop PR #N) in `Detailed discussion`. -## What was done -## Testing -## Breaking changes -## Checklist -## Attribution -``` +**`Detailed discussion` is for implementors and AI agents** — it may get as technical as needed: problem/rationale, code-level specifics, and the sub-sections above. -`Why this PR exists` comes first; the remaining sections follow in that order. Always create PRs as drafts. +`TL;DR` → `User story` → `Scenario` → `Detailed discussion`, in that order. Always create PRs as drafts. -**PR descriptions describe net final state only** — no development history, changelog, or step-by-step iteration/debugging narrative; that belongs in commit messages. Concise final testing/verification results (the `## Testing` section) describe the final state, not history, and are expected. +**PR descriptions describe net final state only** — no development history, changelog, or step-by-step iteration/debugging narrative; that belongs in commit messages. This doesn't conflict with `### Actual behavior`, which describes the pre-existing problem being solved, not the PR's own iteration history. Concise final testing/verification results (the `### Testing` sub-section) describe the final state, not history, and are expected. ### Reviewing a PR @@ -86,9 +77,9 @@ See [pr-review.md](references/pr-review.md) for the full procedure: fetching PR ### Issues -Before creating, search existing issues (open + closed) and PRs for duplicates. If found, show to user and ask before proceeding. Check for issue templates. Always append attribution footer. +Before creating, search existing issues (open + closed) and PRs for duplicates. If found, show to user and ask before proceeding. Check for issue templates first; if one exists, fold its required content into the skeleton linked below rather than replacing it. -**Feature/enhancement issues must include a `### User Story` section** -- an informal story describing what the user can achieve (no technical details). Start with "As a **\**, I want to ... so that ...". Multiple personas are fine. Place user stories before technical details. +Issue bodies use the same plain-language-first skeleton as PRs (see §Creating a PR for the full rationale). Copy the skeleton from [issue-body-template.md](references/issue-body-template.md) (the fenced block only, not the page's title or explanatory text) and fill it in: `TL;DR` → `User story` → `Scenario` → `Detailed discussion`. `User story` uses the same "As a **\**, I want to ..., to achieve ..." shape as PRs — multiple personas are fine, repeat the line. Always append the attribution footer last. ## Safety Rules diff --git a/skills/git-and-github/references/gh-cli-fallback.md b/skills/git-and-github/references/gh-cli-fallback.md index 4b5f296..3e65088 100644 --- a/skills/git-and-github/references/gh-cli-fallback.md +++ b/skills/git-and-github/references/gh-cli-fallback.md @@ -12,41 +12,12 @@ Check for a PR template first: git ls-tree HEAD --name-only -r .github/ | grep -i pull_request_template ``` -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..."). +If a template exists, read and fill it in, folding its required content into the linked skeleton below. See the main skill's §Creating a PR for the full rationale (plain language up top, technical detail in `Detailed discussion`). -Always create PRs as drafts: +Fill in the skeleton from [pr-body-template.md](pr-body-template.md) (the fenced block only, not the page's title or explanatory text), save the completed body to a file, then create the PR as a draft, passing the body via `--body-file` (not `--body` — avoids shell-escaping the whole skeleton inline): ```bash -gh pr create --draft --title ": " --body "$(cat <<'EOF' -## Issue being fixed or feature implemented - -### User Story - -### Details - -Closes # - -## What was done? - - - -## How has this been tested? - - - -## Breaking Changes - -None - -## Checklist - -- [x] I have performed a self-review of my own code -- [x] I have added or updated relevant tests -- [x] I have made corresponding changes to the documentation if needed - -🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent -EOF -)" +gh pr create --draft --title ": " --body-file /path/to/filled-in-pr-body.md ``` ## Reviewing a PR @@ -69,6 +40,12 @@ Check for issue templates before creating: git ls-tree HEAD --name-only -r .github/ | grep -i issue_template ``` +If none exists, fill in the skeleton from [issue-body-template.md](issue-body-template.md) (the fenced block only, not the page's title or explanatory text; see the main skill's §Issues), save the completed body to a file, then: + +```bash +gh issue create --title "" --body-file /path/to/filled-in-issue-body.md +``` + ## Elevated Permissions (ghsudo) -- Optional Fallback If you use a **read-only default token** with `gh`, install [ghsudo](https://github.com/lklimek/ghsudo) (`pip install ghsudo`) for write operations. When a `gh` or `git` command fails with HTTP 403 (Forbidden), 404 (Not Found), or "Resource not accessible", re-run it through ghsudo. **Never fork the repository** — forking creates a separate repo and breaks push/PR workflows. GitHub may return 404 instead of 403 for private resources when the token lacks sufficient permissions. diff --git a/skills/git-and-github/references/issue-body-template.md b/skills/git-and-github/references/issue-body-template.md new file mode 100644 index 0000000..e4f7644 --- /dev/null +++ b/skills/git-and-github/references/issue-body-template.md @@ -0,0 +1,38 @@ +# Issue Body Template + +Canonical issue-body skeleton for `git-and-github` §Issues — same plain-language-first +shape as [`pr-body-template.md`](pr-body-template.md). Copy only the contents of the fenced +block below into the issue body — not this page's title or explanatory text. Drop whole +sections or sub-sections that don't apply, keep the section order. `TL;DR` / `User story` / +`Scenario` are plain-language and user-facing; `Detailed discussion` is technical, for +implementors and AI agents. + +For a bug report, `Scenario` is the reproduction: `Base flow` gets there, `Actual behavior` +is the bug, `Expected behavior` is the fix target. For a feature request with nothing to +reproduce, drop `Scenario` and describe the gap in `User story` / `Detailed discussion` +instead. + +```markdown +**TL;DR:** <one plain-language sentence describing the problem or request> + +## User story +As a **<role>**, I want to <what-to-do>, to achieve <user-goal>. + +## Scenario +### Base flow +<the ordinary steps that lead to this situation — plain narrative> + +### Actual behavior +<what happens today — the bug, gap, or missing capability> + +### Expected behavior +<what should happen, or become possible, once this is addressed> + +## Detailed discussion +<technical notes, proposed approach, logs, links> + +### Prior work +<links to possibly related or similar PRs/issues, each with a one-sentence summary of how it relates to this one; omit if none> + +### Attribution +``` diff --git a/skills/git-and-github/references/pr-body-template.md b/skills/git-and-github/references/pr-body-template.md new file mode 100644 index 0000000..39760aa --- /dev/null +++ b/skills/git-and-github/references/pr-body-template.md @@ -0,0 +1,37 @@ +# PR Body Template + +Canonical PR-body skeleton for `git-and-github` §Creating a PR. Copy only the contents of +the fenced block below into the PR body — not this page's title or explanatory text. Drop +whole sections or sub-sections that don't apply, keep the section order. `TL;DR` / +`User story` / `Scenario` are plain-language and user-facing; `Detailed discussion` is +technical, for implementors and AI agents. + +```markdown +**TL;DR:** <one plain-language sentence describing what this PR does> + +## User story +As a **<role>**, I want to <what-to-do>, to achieve <user-goal>. + +## Scenario +### Base flow +<the ordinary steps that lead to this situation — plain narrative> + +### Actual behavior +<what happens today — the bug, gap, or missing capability> + +### Expected behavior +<what should happen, or become possible, after this PR> + +## Detailed discussion +### What was done +<description of changes; add `Closes #<issue-number>` here if applicable> + +### Testing +### Breaking changes +### Checklist + +### Prior work +<links to possibly related or similar PRs/issues, each with a one-sentence summary of how it relates to this one; omit if none> + +### Attribution +``` diff --git a/skills/push/SKILL.md b/skills/push/SKILL.md index ae533ad..c71d15f 100644 --- a/skills/push/SKILL.md +++ b/skills/push/SKILL.md @@ -30,7 +30,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 + - PR body MUST follow the TL;DR → User story → Scenario → Detailed discussion skeleton 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_issue_body_template.py b/tests/test_issue_body_template.py new file mode 100644 index 0000000..f15441b --- /dev/null +++ b/tests/test_issue_body_template.py @@ -0,0 +1,86 @@ +"""Regression guard: the canonical issue-body template lives in one external file. + +Mirrors `test_pr_body_template.py`: the literal skeleton lives ONLY in +`skills/git-and-github/references/issue-body-template.md`; `git-and-github/SKILL.md` +must reference it (§Issues), not inline a duplicate. Pins the template's heading +set/ordering — same plain-language-first shape as the PR-body template. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +GIT_GITHUB = REPO_ROOT / "skills" / "git-and-github" / "SKILL.md" +TEMPLATE = ( + REPO_ROOT / "skills" / "git-and-github" / "references" / "issue-body-template.md" +) + +USER_STORY = "## User story" +SCENARIO = "## Scenario" +DETAILED = "## Detailed discussion" + +SKELETON_HEADINGS = ( + "**TL;DR:**", + USER_STORY, + SCENARIO, + "### Base flow", + "### Actual behavior", + "### Expected behavior", + DETAILED, + "### Attribution", + "### Prior work", +) + + +def test_template_has_required_headings() -> None: + text = TEMPLATE.read_text(encoding="utf-8") + for heading in SKELETON_HEADINGS: + assert heading in text, f"{TEMPLATE}: missing '{heading}'" + + +def test_template_sections_are_ordered() -> None: + text = TEMPLATE.read_text(encoding="utf-8") + tldr = text.index("**TL;DR:**") + user_story = text.index(USER_STORY) + scenario = text.index(SCENARIO) + base_flow = text.index("### Base flow") + actual = text.index("### Actual behavior") + expected = text.index("### Expected behavior") + detailed = text.index(DETAILED) + attribution = text.index("### Attribution") + prior_work = text.index("### Prior work") + assert tldr < user_story < scenario < detailed, ( + f"{TEMPLATE}: sections must appear in order TL;DR -> User story -> Scenario -> Detailed discussion" + ) + assert scenario < base_flow < actual < expected < detailed, ( + f"{TEMPLATE}: 'Scenario' sub-sections must appear in order " + "Base flow -> Actual behavior -> Expected behavior, before 'Detailed discussion'" + ) + assert detailed < prior_work < attribution, ( + f"{TEMPLATE}: 'Prior work' must sit between 'Detailed discussion' and 'Attribution'" + ) + assert attribution == max(text.index(h) for h in SKELETON_HEADINGS), ( + f"{TEMPLATE}: 'Attribution' (the mandatory footer) must be the last section in the skeleton" + ) + + +def test_skill_references_template_without_inlining_it() -> None: + text = GIT_GITHUB.read_text(encoding="utf-8") + assert "references/issue-body-template.md" in text, ( + f"{GIT_GITHUB}: §Issues must link to references/issue-body-template.md" + ) + + +def test_skill_issues_section_scoped_to_shared_template() -> None: + """§Issues should point at the shared skeleton, not restate a bespoke one.""" + text = GIT_GITHUB.read_text(encoding="utf-8") + issues_heading = text.index("### Issues") + issues_section = text[issues_heading:] + for heading in SKELETON_HEADINGS: + assert ( + re.search(r"^" + re.escape(heading), issues_section, re.MULTILINE) is None + ), ( + f"{GIT_GITHUB}: §Issues must not inline a duplicate '{heading}' heading — delegate to issue-body-template.md" + ) diff --git a/tests/test_pr_body_template.py b/tests/test_pr_body_template.py index db6a1b4..10c0665 100644 --- a/tests/test_pr_body_template.py +++ b/tests/test_pr_body_template.py @@ -1,58 +1,112 @@ -"""Regression guard: the canonical PR-body template must lead with "Why this PR exists". +"""Regression guard: the canonical PR-body template lives in one external file. -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. +The literal skeleton lives ONLY in `skills/git-and-github/references/pr-body-template.md`; +`git-and-github/SKILL.md` and `push/SKILL.md` must reference it, not inline a duplicate. +This test pins the template's heading set/ordering (`TL;DR` -> `## User story` -> +`## Scenario` (`### Base flow` / `### Actual behavior` / `### Expected behavior`) -> +`## Detailed discussion`) and guards against the skeleton drifting back into the skills. """ from __future__ import annotations +import re 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" +TEMPLATE = ( + REPO_ROOT / "skills" / "git-and-github" / "references" / "pr-body-template.md" +) -WHY = "Why this PR exists" +USER_STORY = "## User story" +SCENARIO = "## Scenario" +DETAILED = "## Detailed discussion" +SKELETON_HEADINGS = ( + "**TL;DR:**", + USER_STORY, + SCENARIO, + "### Base flow", + "### Actual behavior", + "### Expected behavior", + DETAILED, + "### What was done", + "### Testing", + "### Breaking changes", + "### Checklist", + "### Attribution", + "### Prior work", +) -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_template_has_required_headings() -> None: + text = TEMPLATE.read_text(encoding="utf-8") + for heading in SKELETON_HEADINGS: + assert heading in text, f"{TEMPLATE}: missing '{heading}'" + + +def test_template_sections_are_ordered() -> None: + text = TEMPLATE.read_text(encoding="utf-8") + tldr = text.index("**TL;DR:**") + user_story = text.index(USER_STORY) + scenario = text.index(SCENARIO) + base_flow = text.index("### Base flow") + actual = text.index("### Actual behavior") + expected = text.index("### Expected behavior") + detailed = text.index(DETAILED) + checklist = text.index("### Checklist") + prior_work = text.index("### Prior work") + attribution = text.index("### Attribution") + assert tldr < user_story < scenario < detailed, ( + f"{TEMPLATE}: sections must appear in order TL;DR -> User story -> Scenario -> Detailed discussion" + ) + assert scenario < base_flow < actual < expected < detailed, ( + f"{TEMPLATE}: 'Scenario' sub-sections must appear in order " + "Base flow -> Actual behavior -> Expected behavior, before 'Detailed discussion'" + ) + assert checklist < prior_work < attribution, ( + f"{TEMPLATE}: 'Prior work' must sit between 'Checklist' and 'Attribution'" ) + assert attribution == max(text.index(h) for h in SKELETON_HEADINGS), ( + f"{TEMPLATE}: 'Attribution' (the mandatory footer) must be the last section in the skeleton" + ) + + +def _heading_at_line_start(text: str, heading: str) -> bool: + """True if `heading` appears as its own line (a real heading), not just quoted in prose.""" + return re.search(r"^" + re.escape(heading), text, re.MULTILINE) is not None -def test_why_section_leads_what_and_testing() -> None: +def test_skill_references_template_without_inlining_it() -> 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'" + assert "references/pr-body-template.md" in text, ( + f"{GIT_GITHUB}: must link to references/pr-body-template.md" + ) + for heading in SKELETON_HEADINGS: + assert not _heading_at_line_start(text, heading), ( + f"{GIT_GITHUB}: must not inline a duplicate '{heading}' heading — delegate to pr-body-template.md" + ) -def test_why_section_demands_reproduction_and_blocking() -> None: - """The skeleton must prompt for a concrete repro/threat scenario and blocking relationship.""" +def test_skill_demands_plain_language_for_user_facing_sections() -> None: + """TL;DR / User story / Scenario must be scoped to plain, user-observable info.""" 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 "plain language" in lowered or "plain-language" in lowered, ( + f"{GIT_GITHUB}: must require plain language in the user-facing PR sections" ) - assert "blocking" in lowered, ( - f"{GIT_GITHUB}: '{WHY}' skeleton must ask for the blocking relationship" + assert "user-observable" in lowered, ( + f"{GIT_GITHUB}: must scope user-facing PR sections to user-observable behavior" ) -def test_push_references_why_without_inlining_template() -> None: +def test_push_references_skeleton_without_inlining_it() -> 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" - ) + for heading in (USER_STORY, SCENARIO, DETAILED): + assert not _heading_at_line_start(text, heading), ( + f"{PUSH}: must not inline a duplicate '{heading}' template — delegate to git-and-github" + )