diff --git a/docs/conventions/config-cascade/CHANGELOG.md b/docs/conventions/config-cascade/CHANGELOG.md index 7f927926ef..974d5c809a 100644 --- a/docs/conventions/config-cascade/CHANGELOG.md +++ b/docs/conventions/config-cascade/CHANGELOG.md @@ -7,6 +7,16 @@ by a pointer line). Per-concern keys and schema are versioned by their own owner change independently. A change to the precedence order or the meaning of a layer is a major bump; adding an optional layer or relaxing a rule additively is a minor bump. +## Deviations and Implementers table, 2026-09-28 + +- **`source-control` `branch_issue_pattern` fail-closed stop declared (#4673).** The Declared list + gains the surface's divergence from rule 4 (degrade soft on a malformed layer): a layer whose + `## branch_issue_pattern` section exists but yields no usable pattern stops resolution with no + issue number, because the value feeds a `Closes #N` line and a lower source's number could close + the wrong issue. The row's conformance cell names the exception. The near-miss-heading stop that + shipped in #4581 was that divergence already, undeclared. No contract rule change, so no version + bump. + ## Implementers table, 2026-09-08 - **`architecture` and `authoring-formats` C4 dialect surfaces (#3910).** The two rows no longer diff --git a/docs/conventions/config-cascade/README.md b/docs/conventions/config-cascade/README.md index 3907561de0..c834b1ac4b 100644 --- a/docs/conventions/config-cascade/README.md +++ b/docs/conventions/config-cascade/README.md @@ -265,6 +265,18 @@ surface, or amend this contract) is a separate human-gated decision. gitignoring one. Claude Code documents local behavior only for surfaces it actually resolves (`CLAUDE.local.md`, `settings.local.json`); a generic `*.local.*` filename has no platform-defined meaning. +- **`source-control` stops `branch_issue_pattern` resolution on an unusable layer (#4673).** + Rule 4 of the resolution algorithm would resolve as if a malformed layer were absent. For this + one key, `parse-branch-issue.sh` instead fails closed: a layer whose `## branch_issue_pattern` + section exists but yields no usable pattern (a near-miss heading, no value, a heading or HTML + comment as the first value line, an empty or unterminated fence, or a pattern that fails + validation) prints a note naming the layer and the reason, emits no issue number, and exits 1. + The consumer is a `Closes #N` line that closes an issue on merge, so a lower layer or the + default supplying a different number is worse than no number; the no-number path already exists + and `/source-control:pull-request create` relays the note. A higher layer that already supplied + a valid pattern still wins, since the lower layer is never read, and a malformed deprecated + `userConfig` value is still ignored with a note. Every other `source-control` key keeps the + soft degrade. **Undeclared**, meaning divergence with no recorded rationale: @@ -281,7 +293,7 @@ convention home, layers → `team, via pointer line`, conformance → the retire | Surface | Consumer config path | Layers | Conformance | |---|---|---|---| -| `source-control` | `.claude/source-control.md` | all three | conforms (per-key override, #660); enforcement reads team-tracked only per [`commit-convention`](../commit-convention/README.md); loop-lane keys (`babysit_loop_*`, read by the source-control babysit lane; the work-items lanes tie in via the loop-lane convention only) ride the same surface, with the merge-rung key in the policy-floor class: standing raises bind from the team-tracked layer only, and the one named single-invocation exception is an explicitly typed argument rather than a config value in any layer, per [`loop-lane`](../loop-lane/README.md) | +| `source-control` | `.claude/source-control.md` | all three | conforms (per-key override, #660), except the declared fail-closed stop on an unusable `branch_issue_pattern` layer (#4673, see Declared); enforcement reads team-tracked only per [`commit-convention`](../commit-convention/README.md); loop-lane keys (`babysit_loop_*`, read by the source-control babysit lane; the work-items lanes tie in via the loop-lane convention only) ride the same surface, with the merge-rung key in the policy-floor class: standing raises bind from the team-tracked layer only, and the one named single-invocation exception is an explicitly typed argument rather than a config value in any layer, per [`loop-lane`](../loop-lane/README.md) | | `toolchain` / `ecosystem-commands` | `.claude/ecosystems/.yaml` | all three | conforms | | `codebase-health` | `.claude/codebase-health.md` | all three | conforms (concatenating, with a declared empty-list opt-out) | | `bugs` | `.claude/bugs.md` | all three | conforms; `lanes` concatenate and deduplicate by lane `name`, with a declared empty-list opt-out that also drops the bundled defaults, and `filing_posture` is a nearest-wins scalar. Keys owned by the plugin's `reference/config.md`, which also partitions them from the plugin's `output_dir` `userConfig` option. That option is never a key in this surface, and a layer declaring it is reported as an inert unknown key. Written (team layer only) by `/bugs:setup apply`, read by `/bugs:scan` | diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index a8b1df7795..97d25a4506 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.61.5", + "version": "0.62.0", "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), /babysit-loop (the loop-lane merge lane: a standing or drain loop that invokes babysit-prs per cycle, configured through repo-scoped babysit_loop_* keys on the layered source-control.md seam, with merge authority human-only until the target repo's tracked config adopts the lane, a gate-proven C2-mechanical baseline once adopted, and standing merge-rung raises binding from the team-tracked layer only, with one named exception, where an invocation line explicitly typing both the autopilot tier keyword and the dedicated raise argument --merge c3-this-run widens that single invocation's merge authority up to C3 behind a fresh independent frontier-tier resolver, while C4-structural and C5-untrusted-provenance stay unconditionally human-merge), /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, which interviews the repo and writes 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 8b75fbccdd..a2aec7492d 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -3,6 +3,12 @@ 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.62.0] - 2026-09-28 + +### Changed + +- **`parse-branch-issue.sh` stops on an unusable `branch_issue_pattern` layer instead of skipping it** ([#4673](https://github.com/melodic-software/claude-code-plugins/issues/4673)). A layer whose `## branch_issue_pattern` section exists but yields no usable pattern now prints a note containing `resolution stopped`, emits no issue number, and exits 1. Before, it was reported and skipped, and a lower layer, the userConfig, or the built-in default could then supply a different number to the `Closes #N` line: with a broken trailing-number team pattern, `feat/12-widget-34` closed #12. The stop covers a heading or HTML comment as the first value line, an empty or unterminated fence, a pattern that breaks a limit, holds a backreference, or does not compile, and a section with no value (previously skipped without a note). The near-miss-heading stop is unchanged. A higher layer that already supplied a valid pattern still wins, and a userConfig value that fails validation is still ignored, with a note, so the default applies. `create.md`, `config-resolution.md`, setup's `SKILL.md`, and `apply-convention.md` describe the stop, and setup's check reports a stopped layer as a FAIL. The repo's config-cascade convention records the stop as a declared deviation from its degrade-soft rule. + ## [0.61.5] - 2026-09-28 ### Fixed diff --git a/plugins/source-control/reference/config-resolution.md b/plugins/source-control/reference/config-resolution.md index 121a23ebf5..f0c4189bf9 100644 --- a/plugins/source-control/reference/config-resolution.md +++ b/plugins/source-control/reference/config-resolution.md @@ -68,15 +68,23 @@ Markdown, one `## ` H2 per key, the value as the section body: - A leading UTF-8 BOM and CRLF line endings are accepted, and so are trailing whitespace and a closing `#` sequence on the heading (`## branch_issue_pattern ##`). Headings inside fenced blocks are ignored. - - A **near-miss heading** stops resolution: an H2 outside a fence whose text contains - `branch_issue_pattern` in any case but is not the exact heading (`## branch_issue_pattern:`, - `## Branch_Issue_Pattern`, `## branch_issue_pattern (ERE)`). The script prints a note and no - issue number, and exits 1. It does not fall back to a lower layer, the userConfig, or the - default, because any of those could close the wrong issue. A higher-precedence layer that - already supplied a valid pattern wins, since the lower layer is never read. - - A layer is reported and skipped, and resolution continues with the next source, when the - section's first value line is a heading or an HTML comment (`\n`^feat/([0-9]+)-`\n' -run_cfg "HTML comment as the first value line: layer skipped with a note" \ - "$WN" "" - "34" 0 'source-control\.md.*comment.*skipped' +run_cfg "HTML comment as the first value line stops resolution over a valid user-global layer" \ + "$WN" "" - "" 1 "source-control\.md.*comment.*${STOPPED}" + +new_case +layer_raw "${CASE_REPO}/.claude/source-control.md" $'## branch_issue_pattern\n\n\n' +run_cfg "HTML comment first line alone: no output, never the default 12" \ + "$WN" "" - "" 1 "source-control\.md.*comment.*${STOPPED}" new_case layer "${CASE_HOME}/.claude/source-control.md" '-([0-9]+)$' layer_raw "${CASE_REPO}/.claude/source-control.md" $'## branch_issue_pattern\n\n```\n^feat/([0-9]+)-\n' -run_cfg "unterminated fence with content: layer skipped with a note" \ - "$WN" "" - "34" 0 'source-control\.md.*unterminated' +run_cfg "unterminated fence with content stops resolution" \ + "$WN" "" - "" 1 "source-control\.md.*unterminated.*${STOPPED}" + +new_case +layer "${CASE_HOME}/.claude/source-control.md" '-([0-9]+)$' +layer_raw "${CASE_REPO}/.claude/source-control.md" $'## branch_issue_pattern\n\n## trailer_policy\n\nnone\n' +run_cfg "section with no value before the next heading stops resolution" \ + "$WN" "" - "" 1 "source-control\.md.*no value.*${STOPPED}" + +new_case +layer_raw "${CASE_REPO}/.claude/source-control.md" $'## branch_issue_pattern\n\n' +run_cfg "section with no value at end of file stops resolution, never the default" \ + "$WN" "" - "" 1 "source-control\.md.*no value.*${STOPPED}" + +new_case +layer_raw "${CASE_REPO}/.claude/source-control.md" $'## branch_issue_pattern\n\n``\n' +run_cfg "value of only backticks stops resolution, never the default" \ + "$WN" "" - "" 1 "source-control\.md.*no value.*${STOPPED}" # Pattern limits, checked before the pattern is compiled or matched. Each -# rejected layer falls through to the user-global `-([0-9]+)$` (34). +# rejected layer stops resolution; the user-global `-([0-9]+)$` (34) and the +# default (12) must both stay unreached. new_case layer "${CASE_HOME}/.claude/source-control.md" '-([0-9]+)$' layer "${CASE_REPO}/.claude/source-control.md" "^feat/([0-9]+)-|$(printf 'z%.0s' {1..190})" -run_cfg "pattern over 200 characters rejected" "$WN" "" - "34" 0 'source-control\.md.*too long' 'zzzzzzzz' +run_cfg "pattern over 200 characters stops resolution" \ + "$WN" "" - "" 1 "source-control\.md.*too long.*${STOPPED}" 'zzzzzzzz' new_case layer "${CASE_HOME}/.claude/source-control.md" '-([0-9]+)$' layer "${CASE_REPO}/.claude/source-control.md" '^feat/x{0,17}([0-9]+)-' -run_cfg "repetition bound over 16 rejected" "$WN" "" - "34" 0 'source-control\.md.*repetition bound' 'x{0,17}' +run_cfg "repetition bound over 16 stops resolution" \ + "$WN" "" - "" 1 "source-control\.md.*repetition bound.*${STOPPED}" 'x{0,17}' new_case layer "${CASE_HOME}/.claude/source-control.md" '-([0-9]+)$' layer "${CASE_REPO}/.claude/source-control.md" '^feat/((1+)+)[0-9]*-' -run_cfg "quantifier on a group holding a quantifier rejected" "$WN" "" - "34" 0 'source-control\.md.*nested quantifier' '(1+)+' +run_cfg "quantifier on a group holding a quantifier stops resolution" \ + "$WN" "" - "" 1 "source-control\.md.*nested quantifier.*${STOPPED}" '(1+)+' new_case layer "${CASE_HOME}/.claude/source-control.md" '-([0-9]+)$' layer "${CASE_REPO}/.claude/source-control.md" '^[a-z]+/((x{0,255}){0,255})([0-9]+)-' -run_cfg "large nested bounded repetition rejected before it is compiled" \ - "$WN" "" - "34" 0 'source-control\.md.*repetition bound' 'x{0,255}' +run_cfg "large nested bounded repetition stops resolution before it is compiled" \ + "$WN" "" - "" 1 "source-control\.md.*repetition bound.*${STOPPED}" 'x{0,255}' + +new_case +layer "${CASE_HOME}/.claude/source-control.md" '-([0-9]+)$' +layer "${CASE_REPO}/.claude/source-control.md" '^feat/([0-9]+)-\1' +run_cfg "backreference team layer stops resolution over a valid user-global layer" \ + "$WN" "" - "" 1 "source-control\.md.*backreference.*${STOPPED}" '\1' new_case run_cfg "nested-quantifier userConfig ignored, default applies" \ diff --git a/plugins/source-control/skills/setup/SKILL.md b/plugins/source-control/skills/setup/SKILL.md index 12be0a4833..3c4add2a8c 100644 --- a/plugins/source-control/skills/setup/SKILL.md +++ b/plugins/source-control/skills/setup/SKILL.md @@ -65,7 +65,10 @@ token means unset), then the built-in `/-` convention. `won by` n `userConfig (deprecated)` with a WARN recommending `apply branch_issue_pattern=`, or `plugin default`. Confirm the row by running `bash "${CLAUDE_PLUGIN_ROOT}/skills/pull-request/scripts/parse-branch-issue.sh" '${user_config.branch_issue_pattern}'` -from `REPO_ROOT` and relaying any stderr note, which names a skipped layer and the reason. The +from `REPO_ROOT` and relaying any stderr note, which names the source and the reason. A note +containing `resolution stopped` means that layer's section exists but yields no usable pattern: +the script prints no number, whatever the lower sources say, so report the row as a FAIL naming +that layer rather than the source a lower rung would have supplied. The single-quoted second argument passes the deprecated userConfig value, so a configuration set only there is reported correctly; the script ignores the literal placeholder when the key is unset. diff --git a/plugins/source-control/skills/setup/reference/apply-convention.md b/plugins/source-control/skills/setup/reference/apply-convention.md index 6dd155ce6f..b994a62630 100644 --- a/plugins/source-control/skills/setup/reference/apply-convention.md +++ b/plugins/source-control/skills/setup/reference/apply-convention.md @@ -81,11 +81,13 @@ file. body already holds one (`(a+)+`, `([a-z]+-)*`), holds a backreference (`\1` through `\9`), does not compile as an ERE, or has no capture group. Check the first three by reading the value, never by compiling it, since compiling a large bounded repetition can exhaust memory. - `parse-branch-issue.sh` applies the same limits and skips a layer that breaks any of the first - five, with a stderr note; a pattern with no capture group is not skipped but can never yield a - number, so the script prints nothing and exits 1. It reads the issue number from the **last** - capture group and prints it only when it is all digits. Write the heading exactly as - `## branch_issue_pattern`: a near-miss heading stops resolution + `parse-branch-issue.sh` applies the same limits: a layer that breaks any of the first five stops + resolution with a stderr note, printing no number rather than falling through to a lower + source; a pattern with no capture group can never yield a number either, so the script prints + nothing and exits 1. It reads the issue number from the **last** capture group and prints it + only when it is all digits. Write the heading exactly as `## branch_issue_pattern`, with the + value on its first non-blank line: a near-miss heading, a section with no value, or a heading or + HTML comment as that line also stops resolution ([config-resolution.md](../../../reference/config-resolution.md)). - **Alone** (no `subject_pattern=`): write or replace only the `## branch_issue_pattern` section of the chosen layer, value in backticks on the first line under the heading (a value that itself