--- name: stack-mode description: 'Shared stack detection and multi-PR iteration contract. Use when commands operate across a whole PR stack (e.g., /pr-review --stack, /fix-pr --stack).' version: 1.9.3 alwaysApply: false category: workflow-automation tags: - git - stacked-diffs - pr - review - fix-pr - iteration tools: [] complexity: medium model_hint: standard estimated_tokens: 1100 dependencies: - sanctum:stack-create - sanctum:stack-push - sanctum:stack-rebase - leyline:git-platform --- # Stack Mode Shared contract for commands that need to operate across a whole stack of dependent PRs in one invocation. Used by `/pr-review --stack` and `/fix-pr --stack`. The goal is one simple thing: when a PR is part of a stack targeting a common base branch, loop the command's normal workflow across every PR in the stack in base-to-tip order and emit one consolidated summary on the stack root. ## When to Use Load this skill when a command accepts a `--stack` flag (or auto-detects stack membership) and needs to iterate its normal workflow across multiple PRs. Do NOT load this skill for single-PR workflows. The per-PR workflow stays unchanged; stack mode wraps it. ## Contract A calling command MUST: 1. Accept a `--stack` boolean flag AND a `--base ` override (default: `master`). 2. Call Step 1 below to resolve stack membership BEFORE running its main workflow. 3. Run its main workflow per PR in the resolved order. 4. Emit per-PR outputs unchanged (thread replies, issue links, etc.). 5. Emit ONE stack-level summary comment on the root PR using the format in Step 4. 6. Respect per-PR Gate rules (every PR still needs its own thread resolution + issue tracking). A calling command MAY: - Auto-detect stack membership and prompt the user rather than requiring `--stack` explicitly. - Short-circuit iteration when a per-PR failure leaves downstream PRs with stale context. ## Required Progress Tracking The caller creates `TodoWrite` items: 1. `stack-mode:membership-resolved` 2. `stack-mode:iteration-complete` 3. `stack-mode:root-summary-posted` ## Step 1: Resolve Stack Membership (`membership-resolved`) Given a starting PR number `$PR_NUM` and optional base `$BASE` (default: `master`), try three strategies in order and stop at the first that yields a stack of size >= 2. ### Strategy A: Branch-name convention (cheapest) ```bash START_HEAD=$(gh pr view "$PR_NUM" \ --json headRefName -q .headRefName) # stack// if [[ "$START_HEAD" =~ ^stack/([^/]+)/.+$ ]]; then FEATURE="${BASH_REMATCH[1]}" PREFIX="stack/${FEATURE}/" # All local branches with the same prefix STACK_BRANCHES=$(git branch --list "${PREFIX}*" \ | sed 's/^[* ]*//' | sort) # Map branches -> PR numbers (skip branches with no PR) STACK_PRS=() for branch in $STACK_BRANCHES; do pr=$(gh pr list --head "$branch" \ --json number --jq '.[0].number') [ -n "$pr" ] && STACK_PRS+=("$pr") done echo "strategy=A prs=${STACK_PRS[*]}" fi ``` ### Strategy B: Stack summary comment (most reliable) `stack-push` posts a `## Stack` markdown table on the root PR with columns `# | Branch | PR`. Parse it: ```bash # Find the root PR of the stack containing $PR_NUM # The current PR may be the root, or may link to it via # a "Part of stack `stack/`" phrase in its body BODY=$(gh pr view "$PR_NUM" --json body -q .body) # Look for a "## Stack" table in THIS PR's comments TABLE=$(gh pr view "$PR_NUM" --json comments \ --jq '.comments[] | select(.body | contains("## Stack")) | .body' | head -1) # If not on this PR, look for the root PR reference if [ -z "$TABLE" ]; then ROOT_REF=$(echo "$BODY" \ | grep -oE 'stack/[^ `]+' | head -1) # ... resolve ROOT_REF -> root PR number and re-fetch fi # Parse the table: extract all `#` from the PR column STACK_PRS=($(echo "$TABLE" \ | grep -oE '#[0-9]+' | tr -d '#' | sort -un)) echo "strategy=B prs=${STACK_PRS[*]}" ``` ### Strategy C: Base-chain walk (fallback) Walk both directions from `$PR_NUM`: - Up: repeatedly resolve `baseRefName` until it equals `$BASE`. Each visited head is a stack member. - Down: find open PRs whose `baseRefName` equals one of the visited heads. Recurse. ```bash # Ascend from $PR_NUM to the root CUR=$PR_NUM CHAIN=($CUR) while true; do BASE_REF=$(gh pr view "$CUR" \ --json baseRefName -q .baseRefName) [ "$BASE_REF" = "$BASE" ] && break PARENT=$(gh pr list --head "$BASE_REF" \ --state open --json number --jq '.[0].number') [ -z "$PARENT" ] && break CHAIN=("$PARENT" "${CHAIN[@]}") CUR=$PARENT done # Descend from the root collecting children ROOT=${CHAIN[0]} FRONTIER=("$ROOT") while [ ${#FRONTIER[@]} -gt 0 ]; do PARENT=${FRONTIER[0]} FRONTIER=("${FRONTIER[@]:1}") PARENT_HEAD=$(gh pr view "$PARENT" \ --json headRefName -q .headRefName) CHILDREN=($(gh pr list --base "$PARENT_HEAD" \ --state open --json number --jq '.[].number')) for c in "${CHILDREN[@]}"; do CHAIN+=("$c") FRONTIER+=("$c") done done STACK_PRS=("${CHAIN[@]}") echo "strategy=C prs=${STACK_PRS[*]}" ``` ### Ordering Regardless of strategy, order the final list **base-to-tip** (root PR first, leaf PR last). The root is the PR whose `baseRefName` equals `$BASE`. ```bash ROOT_PR="" for p in "${STACK_PRS[@]}"; do base=$(gh pr view "$p" --json baseRefName -q .baseRefName) if [ "$base" = "$BASE" ]; then ROOT_PR=$p break fi done ``` If the stack size is 1, the command MUST fall back to single-PR mode and emit a warning: `stack-mode: only one PR found; --stack has no effect, proceeding as single-PR`. ### GitLab / Bitbucket See `Skill(leyline:git-platform)` for `glab` / Bitbucket equivalents of the `gh` calls above. The three strategies translate directly: GitLab MR dependencies replace base-chain walks, and the `glab mr view --output json` shape matches `gh pr view`. ## Step 2: Confirm With User (if not explicit) When the caller did NOT pass `--stack` but Step 1 resolved a stack of size >= 2, print the stack and ask: ``` Detected stack of N PRs rooted at #: 1. # 2. #<p2> <title> ... Run the workflow on the full stack? [y/N] ``` Default to "N" (single-PR mode). With `--stack` explicit, skip this prompt. ## Step 3: Iterate (`iteration-complete`) Run the caller's main workflow once per PR in base-to-tip order: ```bash FAILED_PRS=() for p in "${STACK_PRS[@]}"; do echo "=== stack-mode: PR #$p ($(($idx+1))/${#STACK_PRS[@]}) ===" # Caller hook: run the single-PR workflow on $p # This includes Gate 1 (thread resolution) and # Gate 2 (issue tracking) per the caller's contract. if ! run_single_pr_workflow "$p"; then FAILED_PRS+=("$p") # Stop iteration: downstream PRs may now be stale break fi done ``` If any PR fails, the command MUST stop and report `stack-mode: halted at #<pr>; downstream PRs untouched`. Downstream PRs are intentionally left alone because their context may have shifted. ## Step 4: Post Stack Summary on Root (`root-summary-posted`) After successful iteration, post ONE consolidated summary comment on the root PR. This does NOT replace per-PR summaries: it links them together. ```markdown ## Stack <Command Name> Summary **Stack**: N PRs rooted at #<root>, base `<base-branch>` **Command**: <command> (e.g., /fix-pr, /pr-review) **Run**: <timestamp> | # | PR | Branch | Status | Per-PR Summary | |---|----|--------|--------|----------------| | 1 | #<root> | `stack/<feat>/<root-slice>` | <done/skipped/failed> | [link to comment] | | 2 | #<p2> | `stack/<feat>/<slice-2>` | <done/skipped/failed> | [link to comment] | | ... | ... | ... | ... | ... | ### Cross-PR Notes - <Any observations that spanned multiple PRs> - <Issues created that reference multiple stack PRs> - <Base-PR-merged cascade reminders, if any> ### Next Steps - If root PR merges, run `Skill(sanctum:stack-rebase)` to cascade the base update through descendants. - Reviewers: rebase-triggered stale-review flags on descendants are expected after any force-push. ``` Post via `gh pr comment "$ROOT_PR" --body-file <file>`. ## Failure Modes and Recovery | Failure | Detection | Recovery | |---------|-----------|----------| | Stack of size 1 | Step 1 returns one PR | Emit warning, fall back to single-PR mode | | Base chain loops | Visited set on ascent | Halt, report `stack-mode: cycle detected` | | Mid-stack PR failed its Gates | Caller raises | Halt iteration, leave downstream untouched | | Strategy B table malformed | Parse fails | Fall through to Strategy C | | Auth scope insufficient | `gh` returns 403 | Halt with clear error; caller surfaces to user | ## Notes - Stack mode is ADDITIVE: single-PR behavior is unchanged. Only the iteration and root summary are new. - Per-PR Gate 1 (thread resolution) and Gate 2 (issue tracking) remain unchanged: each PR still passes its own gates independently. - Never merge Gate 1 across the stack. A resolved thread on one PR is not a resolved thread on another. - The root summary is idempotent: re-running stack mode should update (not duplicate) the summary comment. Match on `## Stack <Command Name> Summary` as the key. - `stack-mode` is a read/orchestration skill; it does NOT push, rebase, or edit branches. Those concerns live in `stack-create`, `stack-push`, and `stack-rebase`.