diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index a85b245e3..8b249da8f 100644 --- a/plugins/source-control/.claude-plugin/plugin.json +++ b/plugins/source-control/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "source-control", - "version": "0.15.2", + "version": "0.15.3", "description": "Git and GitHub delivery workflow: /commit (Conventional Commits + Co-Authored-By trailer via safe heredoc mechanics), /pull-request (prep, create, CI monitoring, review-comment triage, merge, CI-log fetch), /babysit-prs (self-pacing fleet loop โ€” safe by default; opt-in worker/autopilot tiers add gate-checked merge and thread resolution behind a deterministic Python engine), /worktree (create, status, cleanup, audit for parallel-session isolation), /setup (check the effective commit-subject / PR-title convention merged across its config layers and the babysit-prs config, or apply โ€” interview the repo and write the convention config to a chosen layer), and /resolve-conflicts (intent-first merge/rebase conflict resolution with a semantic-conflict sweep โ€” never --abort). The commit-subject / PR-title convention is configurable via a source-control.md config written by a re-runnable setup skill, layered across a ~/.claude user-global file, the tracked team file, and a gitignored .claude/source-control.local.md personal overlay merged per key; Conventional Commits is the default when no convention is declared.", "author": { "name": "Melodic Software", diff --git a/plugins/source-control/CHANGELOG.md b/plugins/source-control/CHANGELOG.md index 6df0e2592..e74156754 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -3,6 +3,31 @@ All notable changes to the `source-control` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.15.3] + +### Added + +- **`pull-request` create flow gates the PR-body "Generated with Claude Code" attribution line behind + a config seam (#439).** The `๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)` line + was hardcoded into the PR-body heredoc `/pull-request create` appends to every skill-created PR, with + no config key to change or suppress it โ€” asymmetric with the commit trailer, which `/commit` already + externalizes via `.claude/source-control.md`'s `trailer_policy`. A consumer wanting no Claude + attribution in PR bodies (or a different line) had to fork or hand-edit the plugin, violating the + repo's "configurable without editing the plugin" convention. The line now resolves from a new + `pr_body_attribution` key across the same three `source-control.md` layers (per + `reference/config-resolution.md`): **absent โ†’ the default line (unchanged current behavior, so + existing consumers are unaffected)**, `none` โ†’ the line is omitted, any other value โ†’ that literal + line. A **sibling key rather than a reuse of `trailer_policy`** was chosen deliberately: the two + govern different surfaces (a commit `Co-Authored-By:` trailer vs a Markdown PR-body line), and + overloading `trailer_policy` would have silently stripped the PR-body line from every consumer who + already set `trailer_policy: none` (the plugin's own commit eval fixture is one) โ€” a behavior change + the opt-in-only requirement forbids. `create.md` ยง2.4.1 resolves the effective value at the model + level and splices it in as literal text *outside* the quoted heredoc via the same + parameter-expansion concat `${CLOSES_LINE}` uses, preserving the section's shell-injection safety + (a custom `$`-bearing line stays inert). `/source-control:setup`'s interview, config template, and + `check` render the new key; `pull-request` `SKILL.md` and `config-resolution.md` document it. New + evals pin both the default-present and `pr_body_attribution: none` opt-out paths. + ## [0.15.2] ### Fixed diff --git a/plugins/source-control/reference/config-resolution.md b/plugins/source-control/reference/config-resolution.md index 5b9bce7a0..e0105ae9f 100644 --- a/plugins/source-control/reference/config-resolution.md +++ b/plugins/source-control/reference/config-resolution.md @@ -25,6 +25,11 @@ Markdown, one `## ` H2 per key, the value as the section body: any other casing or punctuation is treated as a pattern in its own right. - `trailer_policy` โ€” the attribution-trailer template, or `none`. Absent means the `/commit` default trailer applies. +- `pr_body_attribution` โ€” the attribution line `/pull-request create` appends to the PR body, or + `none`. Absent means the default `๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)` + line applies. This is the PR-body analogue of `trailer_policy`, and a separate key on purpose: the + two govern different surfaces (a commit `Co-Authored-By:` trailer vs a Markdown PR-body line), so a + consumer setting `trailer_policy: none` keeps the PR-body line unless they also set this to `none`. Absent sections are absent, never empty. diff --git a/plugins/source-control/skills/pull-request/.claude/source-control.md b/plugins/source-control/skills/pull-request/.claude/source-control.md new file mode 100644 index 000000000..912a80060 --- /dev/null +++ b/plugins/source-control/skills/pull-request/.claude/source-control.md @@ -0,0 +1,22 @@ +# source-control configuration + +Commit-subject / PR-title convention for the source-control plugin, resolved by +`/source-control:commit` and `/source-control:pull-request` before they infer from the repo's own +CLAUDE.md/rules/commit-msg hook or fall back to the bundled Conventional Commits default. +Re-run `/source-control:setup` to change these values. + +## subject_pattern + +Conventional Commits + +## type_list + +build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test + +## pr_title_pattern + +Same as `subject_pattern`. + +## pr_body_attribution + +none diff --git a/plugins/source-control/skills/pull-request/SKILL.md b/plugins/source-control/skills/pull-request/SKILL.md index 12f25b71f..1263bc945 100644 --- a/plugins/source-control/skills/pull-request/SKILL.md +++ b/plugins/source-control/skills/pull-request/SKILL.md @@ -30,6 +30,8 @@ Consumer conventions come from the consuming project's own `CLAUDE.md`, `AGENTS. **PR title format** resolves via the same ladder `/commit` uses for the commit subject (see its SKILL.md), checked in order: the `pr_title_pattern` resolved across all three layers of the `source-control.md` convention config per [../../reference/config-resolution.md](../../reference/config-resolution.md) โ†’ the consuming project's own `CLAUDE.md`/`AGENTS.md`/rules โ†’ Conventional Commits (11-type vocabulary โ€” `build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test`; confirmed via the spec, the Angular convention, commitlint's `@commitlint/config-conventional`, and `amannn/action-semantic-pull-request`'s default `types` โ€” `security` is not a member of any of these) as the default. See [reference/create.md](reference/create.md) ยง2.4.1 for where the title is derived. When no layer resolves a `pr_title_pattern` and nothing is inferable, point the user at `/source-control:setup`. +**PR-body attribution** (the `๐Ÿค– Generated with [Claude Code]โ€ฆ` line) resolves from the `pr_body_attribution` key across the same three `source-control.md` layers ([../../reference/config-resolution.md](../../reference/config-resolution.md)) โ€” absent โ†’ the default line, `none` โ†’ omitted, any other value โ†’ that line. It is the PR-body analogue of `/commit`'s `trailer_policy` and gated separately; see [reference/create.md](reference/create.md) ยง2.4.1 for the assembly. + ## Emit checklist For PR lifecycle runs spanning 3+ phases, copy `${CLAUDE_PLUGIN_ROOT}/skills/pull-request/templates/checklist.md` into your project's working-notes location (or track it inline) and tick each `- [ ]` as the phase produces its output. Stateful surface; survives `/clear`. diff --git a/plugins/source-control/skills/pull-request/evals/evals.json b/plugins/source-control/skills/pull-request/evals/evals.json index e495bc302..b55522d25 100644 --- a/plugins/source-control/skills/pull-request/evals/evals.json +++ b/plugins/source-control/skills/pull-request/evals/evals.json @@ -113,6 +113,31 @@ "It stashes tracked and untracked changes with git stash push -u", "It reports the stash and restoration command to the user" ] + }, + { + "id": 9, + "name": "pr-body-attribution-default-present", + "prompt": "/pull-request create from branch feat/512-add-widget โ€” no source-control.md config file exists in this repo. Assemble and create the PR.", + "expected_output": "With no source-control.md config declaring pr_body_attribution, the skill resolves the key as absent across all three layers and keeps the current default behavior: the assembled PR body ends with the `๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)` attribution line (ยง2.4.1). Existing consumers see no change. The line is spliced as literal text via parameter-expansion concat, not embedded in the quoted heredoc.", + "files": [], + "expectations": [ + "The assembled PR body includes the default `๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)` attribution line when no pr_body_attribution key is configured", + "The default is the unchanged current behavior โ€” absence of the key is not treated as opt-out", + "The attribution line is added by resolving the pr_body_attribution key (per config-resolution.md), not hardcoded unconditionally into the body" + ] + }, + { + "id": 11, + "name": "pr-body-attribution-opt-out-suppressed", + "prompt": "/pull-request create from branch feat/512-add-widget. Assemble and create the PR.", + "expected_output": "With .claude/source-control.md declaring `pr_body_attribution: none`, the skill resolves that key across the three source-control.md layers (per reference/config-resolution.md, the same seam /commit's trailer_policy uses) and omits the `๐Ÿค– Generated with [Claude Code]โ€ฆ` attribution line from the assembled PR body entirely โ€” the ATTRIBUTION value resolves to empty and the trailing concat is skipped. The `## Summary`, `## Test plan`, and `## Related` sections and the `${CLOSES_LINE}` closing keyword are unaffected. trailer_policy is a separate key: this opt-out governs only the PR body, not the commit trailer.", + "files": [".claude/source-control.md"], + "expectations": [ + "pr_body_attribution: none from .claude/source-control.md is honored: the PR body carries no `๐Ÿค– Generated with [Claude Code]โ€ฆ` attribution line", + "The suppression is driven by resolving pr_body_attribution across the source-control.md layers, not by editing the plugin", + "The rest of the PR body (Summary, Test plan, Related, and the closing-keyword line) is assembled unchanged", + "Suppressing the PR-body attribution is treated as independent of trailer_policy โ€” the opt-out targets only the PR body" + ] } ] } diff --git a/plugins/source-control/skills/pull-request/reference/create.md b/plugins/source-control/skills/pull-request/reference/create.md index 947cc3760..8ab16285d 100644 --- a/plugins/source-control/skills/pull-request/reference/create.md +++ b/plugins/source-control/skills/pull-request/reference/create.md @@ -160,7 +160,7 @@ Persist chosen line(s) into `${CLOSES_LINE}`. NEVER wrap a closing keyword in an git push -u origin ``` -Derive PR title from the commit subject, shaped to satisfy the resolved subject/title convention (the ladder in [SKILL.md](../SKILL.md): layered `source-control.md` config โ†’ project convention โ†’ Conventional Commits default). Build body with `${CLOSES_LINE}` at top, followed by Summary + Test plan + a `## Related` section + Claude Code attribution: +Derive PR title from the commit subject, shaped to satisfy the resolved subject/title convention (the ladder in [SKILL.md](../SKILL.md): layered `source-control.md` config โ†’ project convention โ†’ Conventional Commits default). Build body with `${CLOSES_LINE}` at top, followed by Summary + Test plan + a `## Related` section + a config-gated attribution line: ```bash # Quoted heredoc โ€” body template is inert; nothing inside expands. @@ -174,23 +174,44 @@ TEMPLATE=$(cat <<'EOF' ## Related N/A - -๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) EOF ) -# Concat CLOSES_LINE in front of TEMPLATE via bash parameter expansion. -# Parameter expansion of "${VAR}" does NOT re-evaluate the expanded value -# โ€” if CLOSES_LINE contains literal "$(rm -rf ~)" (e.g. user typed it into -# the orphan-PR or multi-issue prompt), it stays a literal string and is -# never executed. This is the defense against shell injection through -# user-supplied prompt input. +# Resolve the PR-body attribution line from the `pr_body_attribution` key across +# the three source-control.md layers (../../reference/config-resolution.md), the +# same seam `/commit`'s `trailer_policy` uses for the commit trailer. Absent โ†’ the +# default line (current behavior โ€” existing consumers are unaffected); a value of +# `none` โ†’ omit the line; any other value โ†’ that literal line. Resolve the effective +# value at the model level and bake it in as literal text below; do NOT reference it +# as an unexpanded shell var inside the quoted heredoc (a quoted heredoc emits +# `${ATTRIBUTION}` verbatim), and do NOT switch the heredoc to unquoted `< ATTRIBUTION="" (omit the line) +# pr_body_attribution: -> ATTRIBUTION='' (SINGLE-quoted, NEVER +# double-quoted: bash command-substitutes $(โ€ฆ) inside a +# double-quoted assignment RHS at assignment time, so a +# $()-bearing custom value would execute here โ€” single- +# quoting keeps it inert at the assignment site. Escape any +# literal single quote as '\'' โ€” e.g. ATTRIBUTION='it'\''s ok'. +# The concat below is also inert, but assignment is the +# first line of defense.) + +# Concat CLOSES_LINE in front of TEMPLATE and ATTRIBUTION after it, via bash +# parameter expansion. Parameter expansion of "${VAR}" does NOT re-evaluate the +# expanded value โ€” if CLOSES_LINE contains literal "$(rm -rf ~)" (e.g. user typed +# it into the orphan-PR or multi-issue prompt), or ATTRIBUTION carries a configured +# `$`-bearing custom line, it stays a literal string and is never executed. This is +# the defense against shell injection through user-supplied prompt input and +# configured text. BODY="" [[ -n "$CLOSES_LINE" ]] && BODY="${CLOSES_LINE}"$'\n\n' BODY+="$TEMPLATE" +[[ -n "$ATTRIBUTION" ]] && BODY+=$'\n\n'"$ATTRIBUTION" ``` -**Why quoted heredoc + concat (not `< + + ## pr_body_attribution + + ``` Drop any section with no content rather than leaving it empty. Writing a non-`team` layer, add one