Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/source-control/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
25 changes: 25 additions & 0 deletions plugins/source-control/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions plugins/source-control/reference/config-resolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ Markdown, one `## <key>` 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.

Expand Down
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions plugins/source-control/skills/pull-request/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
25 changes: 25 additions & 0 deletions plugins/source-control/skills/pull-request/evals/evals.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
]
}
41 changes: 31 additions & 10 deletions plugins/source-control/skills/pull-request/reference/create.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ Persist chosen line(s) into `${CLOSES_LINE}`. NEVER wrap a closing keyword in an
git push -u origin <branch-name>
```

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.
Expand All @@ -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 `<<EOF` to
# force expansion β€” that would re-evaluate the whole body and reopen the injection
# hole this section is built to close.
ATTRIBUTION='πŸ€– Generated with [Claude Code](https://claude.com/claude-code)' # key absent β†’ default
# pr_body_attribution: none -> ATTRIBUTION="" (omit the line)
# pr_body_attribution: <custom text> -> ATTRIBUTION='<that text>' (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 `<<EOF`):** unquoted heredoc `<<EOF` evaluates `$(...)`, `${...}`, and `` `...` `` *inside the body content itself* (POSIX heredoc semantics β€” `<<EOF` is treated as if double-quoted). If `${CLOSES_LINE}` ever contains shell-meta from interactive prompt input, an unquoted heredoc would execute it. Quoted `<<'EOF'` is inert; splicing `${CLOSES_LINE}` via parameter expansion + concat keeps user input as literal text.
**Why quoted heredoc + concat (not `<<EOF`):** unquoted heredoc `<<EOF` evaluates `$(...)`, `${...}`, and `` `...` `` *inside the body content itself* (POSIX heredoc semantics β€” `<<EOF` is treated as if double-quoted). If `${CLOSES_LINE}` ever contains shell-meta from interactive prompt input, or `${ATTRIBUTION}` carries a configured custom line, an unquoted heredoc would execute it. Quoted `<<'EOF'` is inert; splicing `${CLOSES_LINE}` and `${ATTRIBUTION}` via parameter expansion + concat keeps both as literal text. The attribution line is deliberately spliced *outside* the heredoc rather than embedded inside it so that a `pr_body_attribution` value resolved from config never re-enters shell evaluation.

`gh pr create --body` fully overrides `.github/PULL_REQUEST_TEMPLATE.md` (cli/cli #10751) β€” body assembly above is the canonical path for skill-driven PRs; the template is the web-UI backstop. When the consuming project ships a PR template, mirror its section shape in the assembled body.

Expand Down
19 changes: 15 additions & 4 deletions plugins/source-control/skills/setup/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ single layer's value as the effective convention; a reader who cannot see which
tell why `/commit` behaves as it does.

```text
key value won by
subject_pattern ^[A-Z]+-\d+: .+ team
pr_title_pattern Same as subject_pattern team
trailer_policy none local overlay
key value won by
subject_pattern ^[A-Z]+-\d+: .+ team
pr_title_pattern Same as subject_pattern team
trailer_policy none local overlay
pr_body_attribution none local overlay
```

Per-layer verdicts:
Expand Down Expand Up @@ -216,6 +217,11 @@ With no argument in an interactive session, run the interview:
- **`trailer_policy`** (optional) β€” whether commits should carry a `Co-Authored-By:` (or other)
attribution trailer, and its exact template. Recommend keeping `/commit`'s default unless the
user states otherwise. Omit this section entirely if the repo has no trailer convention.
- **`pr_body_attribution`** (optional) β€” the attribution line `/pull-request create` appends to the
PR body, the PR-body analogue of `trailer_policy` and gated separately (a consumer setting
`trailer_policy: none` still keeps the PR-body line unless this is also set). Recommend keeping the
default `πŸ€– Generated with [Claude Code]…` line unless the user wants a custom line or `none` to
omit it. Omit this section entirely to keep the default.
5. **Write the config.** Materialize the target layer's path with these sections:

```markdown
Expand Down Expand Up @@ -243,6 +249,11 @@ With no argument in an interactive session, run the interview:
## trailer_policy

<only present if the repo has a trailer convention>

## pr_body_attribution

<only present if the repo overrides the default PR-body attribution line β€” a custom line, or
`none` to omit it>
```

Drop any section with no content rather than leaving it empty. Writing a non-`team` layer, add one
Expand Down
Loading