name: hdb:split-pr description: Decompose an oversize branch into multiple focused PRs with dependency ordering and detailed task lists
hdb:split-pr
Decompose an oversize branch into multiple focused, non-overlapping PRs with correct dependency ordering and detailed recovery tasks.
Usage
/hdb:split-pr [--base <branch>] [--dry-run]
IMPORTANT
This skill produces two artifacts: (1) a partition plan mapping every change to exactly one PR, and (2) a detailed task list per PR that captures specific changes from the original branch. The goal is maximum recovery of past work with no overlap between PRs.
Description
When a branch accumulates too many changes for a single reviewable PR (rough threshold: >15 files changed, >500 net insertions, or >3 distinct themes), this skill decomposes it into multiple smaller PRs. Each PR is coherent (single theme), self-contained (passes tests independently), and non-overlapping (no file change appears in two PRs). The PRs are ordered so dependencies are satisfied: PR A merges before PR B if B depends on changes from A.
After partitioning, the skill generates detailed tasks for each PR — each task references specific files, original commits, and code from the source branch so the implementer can recover the exact changes efficiently.
Instructions
When the user invokes /hdb:split-pr [--base <branch>] [--dry-run]:
Phase 1: Inventory
Catalog everything on the branch before making any decisions.
Determine the base branch. If
--baseis provided, use it. Otherwise detect:git merge-base master HEADIf neither
masternormainexists, ask the user to specify the base.List all commits on the branch since divergence:
git log --oneline --reverse $(git merge-base <base> HEAD)..HEADRecord the count. If there are fewer than 5 commits and fewer than 10 files changed, inform the user the branch is small enough for a single PR and stop unless they insist.
List all changed files with diff stats:
git diff --stat <base>..HEAD git diff --name-only <base>..HEAD | sortRead every commit message and categorize each commit by its logical theme. Use the commit message prefix (e.g.,
security:,feat:,fix:,docs:,test:,refactor:,chore:) as the primary signal. When the prefix is ambiguous, read the commit's diff:git show --stat <sha>Build the theme inventory table. Present it to the user for review:
Theme Commits Files Net Lines Description rate-limiting 8 5 +320 In-memory and Redis rate limiter webhook-security 6 4 +180 HMAC verification, payload limits ... ... ... ... ... TOTAL 62 43 +1913 Identify cross-cutting files — files touched by commits from multiple themes:
# For each file, list which themes' commits touch it git log --format="%h %s" <base>..HEAD -- <file>Present cross-cutting files in a separate table:
File Themes Decision Needed backend/app/core/config.pyrate-limiting, webhook-security Which PR owns the config changes? backend/app/main.pyrate-limiting, auth-refactor Middleware registration spans two features Wait for user confirmation of the theme groupings before proceeding. The user may want to merge themes, split themes, or rename them.
Phase 2: Dependency Analysis
Determine the merge order.
For each theme group, identify dependencies on other groups. A theme depends on another if:
- It imports or calls functions introduced by the other theme
- It modifies files whose baseline state requires the other theme's changes
- Its tests rely on fixtures or utilities introduced by the other theme
- It extends a database migration introduced by the other theme
Check dependencies by reading the diffs:
git diff <base>..HEAD -- <file>Build the dependency DAG. Present it as a table and an ASCII diagram:
PR Theme Depends On Merge Order 1 auth-refactor (none) First 2 rate-limiting auth-refactor Second 3 webhook-security auth-refactor Second (parallel with 2) 4 docs rate-limiting, webhook-security Third 1:auth-refactor ├── 2:rate-limiting ├── 3:webhook-security └── 4:docs (after 2+3)Detect cycles. If the dependency graph has a cycle, the themes cannot be cleanly separated. Options:
- Merge the cyclic themes into a single PR
- Extract shared infrastructure into a new "foundation" PR that breaks the cycle
Present the cycle to the user and recommend a resolution.
Wait for user confirmation of the dependency ordering before proceeding.
Phase 3: Partition
Assign every file change to exactly one PR.
Assign unambiguous files first. Files touched by only one theme go into that theme's PR. This handles the majority of files.
Resolve cross-cutting files. For each cross-cutting file, choose one strategy:
Strategy When to Use How Assign to earliest PR The file has a clear "foundation" change that later PRs build on Put all changes in the earliest PR in the dependency chain Split by hunk The file has distinct, non-overlapping hunks for each theme Extract per-theme patches and partition hunks Assign to infrastructure PR The file is shared config or wiring (e.g., main.py,config.py)Create a dedicated "infrastructure" PR or assign to the first PR Sequential layering One theme adds a function, another modifies it Put the addition in the earlier PR; the modification in the later PR (which rebases on top) Handle special file categories:
Category Strategy Formatting/linting commits Assign each formatted file to whichever PR already owns that file. If a formatting commit touches files across multiple PRs, split it. Test files Move test files with the feature they test. If a test file covers multiple features, assign it to the latest PR in the dependency chain or create a dedicated test PR. Documentation Small doc changes travel with their feature. Large doc additions (new files, multi-file updates) go in a dedicated docs PR at the end of the chain. Migration files Migrations travel with the schema change they implement. If two migrations exist, they go in separate PRs only if independent. Chain down_revisionvalues accordingly.Docker/CI/Makefile Infrastructure config goes in the earliest PR that needs it, or in a dedicated infrastructure PR. Build the partition table. Present it to the user:
PR Branch Name Theme Files Cherry-Pick Candidates 1 security/auth-refactorAuth renaming deps.py,admin_access.py, ...00102f8,f102b502 security/rate-limitingRate limiting rate_limit.py,rate_limit_backend.py, ...603c653,0d5f4bf, ...... ... ... ... ... Verify completeness. The union of all file lists must equal the original changed file list. No file may appear in two PRs:
Original files: 43 Assigned files: 43 (sum across all PRs) Duplicates: 0 Unassigned: 0If there are unassigned files or duplicates, resolve them before proceeding.
Wait for user confirmation of the partition before generating tasks.
Phase 4: Task Generation
For each PR, generate a detailed task list that captures the specific changes from the original branch.
For each PR, generate ordered tasks. Each task must have:
- Subject — imperative action (e.g., "Add Redis rate limiter module")
- Description — specific file paths, what to change, and references to original commits/diffs:
Create `backend/app/core/rate_limit.py` with: - RateLimiter ABC with async `is_allowed(key: str) -> bool` - InMemoryRateLimiter using sliding-window deque - RedisRateLimiter using sorted-set pipeline - Shared client factory `_get_async_redis(url)` - `create_rate_limiter()` factory function Reference: commits 603c653, 32f5453, 294f518 on chore-security-review Recovery: `git show chore-security-review:backend/app/core/rate_limit.py` - Acceptance criteria — how to verify the task is done:
- File exists and passes `mypy --strict` - `uv run pytest tests/test_rate_limit.py` passes (15 tests)
Task ordering within each PR:
Order Task Type Example 1 Infrastructure / new modules Create rate_limit_backend.pyenum2 Core implementation Create rate_limit.pywith ABC + both backends3 Configuration Add settings to config.py4 Integration / wiring Wire rate limiter into agent_auth.py,board_webhooks.py5 Tests Create test_rate_limit.pywith 15 tests6 Documentation Update security.md,api.md7 Linting / formatting Run ruff,black,isortand fix8 Commit and verify Run full test suite, commit Include recovery commands in each task so the implementer can pull exact code from the source branch:
# View the final state of a file on the source branch git show <source-branch>:<file-path> # View a specific commit's changes to a file git show <sha> -- <file-path> # Extract a file from the source branch into the working tree git checkout <source-branch> -- <file-path> # Generate a patch for specific files git diff <base>..<source-branch> -- <file-path> > /tmp/partial.patchPresent the complete task list to the user, organized by PR:
## PR 1: security/auth-refactor (Auth Renaming) Depends on: (none) | Base: master 1. Rename `require_admin_auth` → `require_user_auth` in deps.py Recovery: git show chore-security-review:backend/app/api/deps.py Acceptance: All 10 API modules import the new name; tests pass 2. Update all API modules to use renamed dependency Files: activity.py, agents.py, approvals.py, ... Recovery: git diff master..chore-security-review -- backend/app/api/activity.py Acceptance: `rg require_admin_auth backend/` returns 0 matches 3. Run tests and commit Acceptance: `uv run pytest` passes; commit message follows conventions ## PR 2: security/rate-limiting (Rate Limiting) Depends on: PR 1 | Base: security/auth-refactor 1. Create rate_limit_backend.py enum ...
Phase 5: Extract (on user request)
Create the sub-PR branches. Work in dependency order (roots first, leaves last).
Decide extraction method per PR:
Method When to Use Pros Cons Cherry-pick Commits are clean and atomic; each commit belongs entirely to one PR Preserves history Fails when commits span multiple themes File-level checkout Commits are mixed or squashed Clean result regardless of commit structure Loses original commit granularity In practice, most splits use file-level checkout because real branches rarely have perfectly atomic commits.
For each PR (in dependency order):
a. Create a new branch from the appropriate base:
# For the first PR (no dependencies): git checkout -b <branch-name> <base> # For PRs that depend on a prior PR: git checkout -b <branch-name> <prior-pr-branch>b. Apply the changes using the chosen method:
Cherry-pick method:
git cherry-pick <sha1> <sha2> <sha3>File-level checkout method:
git checkout <source-branch> -- <file1> <file2> <file3> git commit -m "<theme>: apply <description>"Hunk-level extraction (for cross-cutting files):
git diff <base>..<source-branch> -- <file> > /tmp/partial.patch # Edit the patch to keep only relevant hunks git apply /tmp/partial.patchc. Run tests to verify the branch works standalone:
cd backend && uv run pytest tests/ -x -q # Python go test ./... # Go cargo test # Rustd. Run linters/formatters and fix any issues.
e. Verify the diff is limited to this PR's assigned files:
git diff --name-only <base-of-this-pr>..HEADIf
--dry-runwas specified, stop after generating the partition table and task list. Do not create branches.
Phase 6: Verify
Confirm completeness and correctness after all branches are created.
Verify coverage. For each file in the original diff, confirm it appears in exactly one sub-PR:
for branch in <branch1> <branch2> ...; do echo "=== $branch ===" git diff --name-only <base>..$branch doneVerify each branch passes tests independently.
Generate the final summary:
## Split Summary Original branch: chore-security-review (62 commits, 43 files, +1913/-85) Sub-PRs created: 5 | # | Branch | Theme | Files | Lines | Depends On | Tests | Tasks | |---|--------|-------|-------|-------|------------|-------|-------| | 1 | security/auth-refactor | Auth renaming | 12 | +120 | -- | PASS | 3 | | 2 | security/rate-limiting | Rate limiting | 8 | +450 | #1 | PASS | 8 | | 3 | security/webhook-hardening | Webhook security | 8 | +280 | #1 | PASS | 7 | | 4 | security/docker-hardening | Docker non-root | 3 | +30 | -- | PASS | 2 | | 5 | security/docs-and-tests | Docs + tests | 12 | +550 | #2, #3 | PASS | 5 | Merge order: 1 and 4 (parallel) → 2 and 3 (parallel) → 5 Coverage: 43/43 files assigned. 0 duplicates. 0 unassigned. Total tasks: 25
Phase 7: PR Creation (optional, on user request)
Create PRs in dependency order:
git checkout <branch> git push -u origin <branch> gh pr create --base <target-base> --title "<theme>: <concise description>" --body "$(cat <<'EOF' ## Summary <1-3 bullet points> ## Context PR N of M from the `<original-branch>` split. **Merge order:** <where this fits> **Depends on:** <dependency PR links, or "None"> ## Test plan - [ ] All existing tests pass - [ ] <specific items> Generated with [Claude Code](https://claude.com/claude-code) EOF )"Link dependency PRs in each PR description so reviewers understand the merge order.
Decision Guide: Cross-Cutting Files
| File Type | Resolution |
|---|---|
config.py / settings.py |
Put in the earliest PR that needs the config. Later PRs add their settings on top via rebase. |
main.py / app wiring |
Put in a dedicated infrastructure PR, or in the earliest PR. |
Makefile / docker-compose.yml |
Put in the PR whose feature the change supports. |
| Shared test fixtures | Put in the earliest PR. Later PRs import from the fixture. |
__init__.py / re-exports |
Put in the PR that adds the module being re-exported. |
| Migration files | Always travel with the schema change they implement. |
Common Pitfalls
1. Forgetting to rebase after merge
Later PRs were branched from earlier PR branches. After PR 1 merges into master, PR 2 must rebase:
git checkout security/rate-limiting
git rebase master
git push --force-with-lease
Failure to rebase causes the later PR to include the earlier PR's diff, confusing reviewers.
2. Tests that import across PR boundaries
If PR 2's tests import a helper added by PR 1, the tests fail when PR 2 is checked out before PR 1 merges. Solutions:
- Branch PR 2 from PR 1's branch (not from
master) - Duplicate the helper in PR 2 (acceptable for small helpers)
- Move the helper to a shared utility in PR 1
3. Migration ordering across PRs
If PR 2 and PR 3 both add Alembic migrations, they both have the same down_revision. When both merge, this creates multiple heads. Solutions:
- Put all migrations in one PR
- Use
alembic merge headsafter the second PR merges - Chain the migrations: PR 3's migration depends on PR 2's
4. Formatting commits that touch everything
A single ruff format . commit touches every file. Do not put this in one PR. Instead:
- Re-run the formatter independently on each sub-PR branch
- Let each PR's files be formatted within that PR
5. Losing changes in the split
Always verify coverage (Phase 6, step 25). The most common cause: a file touched by two themes is assigned to only one PR, and the other theme's hunks are dropped.
6. Over-splitting
Not every theme needs its own PR. If two themes are small (< 3 files, < 50 lines each) and naturally related, merge them. The goal is reviewable PRs, not maximally granular PRs.
Size Guidelines
| Original Size | Recommended Split |
|---|---|
| 5-15 files, < 500 lines | Single PR (no split needed) |
| 15-30 files, 500-1000 lines | 2-3 PRs |
| 30-50 files, 1000-2000 lines | 3-5 PRs |
| 50+ files, 2000+ lines | 5-8 PRs (consider deferring some changes) |
Guidelines
- Get confirmation at each gate. Present the theme inventory, dependency DAG, and partition table to the user before proceeding. Backtracking after branch creation is expensive.
- Dependency order is non-negotiable. Every PR must pass tests independently when applied on top of its declared dependencies.
- Prefer file-level checkout over cherry-pick. Real branches rarely have perfectly atomic commits.
- Cross-cutting files go to the earliest PR. When in doubt, put shared files in the first PR in the dependency chain.
- Every task references recovery commands. The implementer should be able to pull exact code from the source branch for each task.
- Run tests on every sub-PR branch. Do not assume a clean partition means clean tests.
- Do not over-split. Fewer, larger PRs are better than many tiny PRs that create a long merge queue. Target 3-6 PRs.
- Document the merge order in every PR. Reviewers need to know "this is PR 2 of 5, depends on PR 1."
- Delete the original branch after all sub-PRs merge. It is dead weight after the split.
- Respect CLAUDE.md. The project's instructions override everything in this skill.
See Also
/hdb:design— design a feature with a PRD and task list (use before implementing, to prevent oversize branches)/roborev:pull-request-reviewer— pre-review a PR before submitting/hdb:alembic— diagnose and fix migration issues that arise during splits