Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
143 commits
Select commit Hold shift + click to select a range
b701c9f
fix: render multi-parent groups once in the dependency tree
vitali87 Aug 30, 2026
cd7ee80
fix: reject mode-only changes instead of silently dropping them
vitali87 Aug 30, 2026
3f6dfc3
fix: decide the full-render parent at traversal time
vitali87 Aug 30, 2026
06b8b29
fix: allow non-fast-forward updates when re-fetching fork refs
vitali87 Aug 30, 2026
5339863
fix: only treat parents reachable from a root as pending
vitali87 Aug 30, 2026
3de2dd0
fix: list per-file global hunk indices in chunk prompt summaries
vitali87 Aug 30, 2026
18fbec9
fix: mark a node as drawn on entry so mid-walk ancestors are not pending
vitali87 Aug 30, 2026
b7f9057
fix: make split branch namespaces ref-safe and collision-free
vitali87 Aug 30, 2026
89e642c
docs: include contents: read in the README action example
vitali87 Aug 30, 2026
68a5598
fix: render a multi-parent node on its last visit by counting parents
vitali87 Aug 30, 2026
f6a8168
fix: stop the score action from clobbering the base ref
vitali87 Aug 30, 2026
0be0e3c
fix: refresh a stale score comment when the PR shrinks below the thre…
vitali87 Aug 30, 2026
c4afe09
docs: describe the action's objective output as the simplified score
vitali87 Aug 30, 2026
23413f0
fix: count distinct parents so duplicate depends_on entries render
vitali87 Aug 30, 2026
c83487b
fix: only write the within-limits comment body on the under-threshold…
vitali87 Aug 30, 2026
c874975
fix: write a file truncated to empty instead of deleting it
vitali87 Aug 30, 2026
9006f31
fix: apply the diff's target file mode in sub-PR worktrees
vitali87 Aug 30, 2026
ab2d2a5
fix: only apply regular-file modes; leave symlink entries alone
vitali87 Aug 30, 2026
fe6a533
fix: apply the target mode to a file truncated to empty too
vitali87 Aug 30, 2026
55f02e8
fix: materialize symlinks as links instead of regular files
vitali87 Aug 30, 2026
fb198cc
fix: replace an existing regular file when the diff turns it into a s…
vitali87 Aug 30, 2026
8eba6d3
fix: exit 1 when a PR's state could not be fetched during merge
vitali87 Aug 30, 2026
8afe7d4
fix: surface a failed OpenAI response with its error message
vitali87 Aug 30, 2026
47a6d1b
docs: pin the README action example to a release tag
vitali87 Aug 30, 2026
33c21fb
fix: fall back to the error code or 'unknown error' for a failed Open…
vitali87 Aug 30, 2026
cee40be
fix: report polling fetch errors and rank fetch_error above unmerged_…
vitali87 Aug 30, 2026
80f038a
test: cover the error-code fallback for a failed OpenAI response
vitali87 Aug 30, 2026
ee2059a
fix: list a PR closed during the auto-merge wait as skipped
vitali87 Aug 30, 2026
93884aa
fix: only warn about truncation when the stop reason is max_tokens
vitali87 Aug 30, 2026
4d1afb5
fix: fail the merge run when a queued PR is closed during the auto-me…
vitali87 Aug 30, 2026
f0223a8
fix: report a hunk listed twice in one group as a duplicate, not an o…
vitali87 Aug 30, 2026
7b4f14f
fix: keep split --dry-run non-destructive and report a typo'd branch …
vitali87 Aug 30, 2026
92103d1
fix: escape plan text in the editor's show output so brackets are not…
vitali87 Aug 30, 2026
2138a57
fix: refuse to split an empty diff and to execute a plan with no changes
vitali87 Aug 30, 2026
4a020d3
fix: escape plan text in the plan table, dependency tree and editor m…
vitali87 Aug 30, 2026
48dceca
fix: escape plan text in the status table, merge summaries and error …
vitali87 Aug 30, 2026
bb52d13
fix: validate split settings before the existing-plan cleanup closes …
vitali87 Aug 30, 2026
58553b0
fix: reject a non-positive --cp-sat-timeout instead of reporting an i…
vitali87 Aug 30, 2026
1ff0284
fix: tell the LLM planner about --min-loc instead of only checking it…
vitali87 Aug 30, 2026
1eb4312
fix: report a CP-SAT time-out distinctly from an infeasible model
vitali87 Aug 30, 2026
d4d7d0a
fix: open PRs in base-chain order and skip children whose parent PR c…
vitali87 Aug 30, 2026
756b68d
fix: let a split re-run after a killed run left stale worktree regist…
vitali87 Aug 30, 2026
5bd7c4f
fix: keep score comment table rows intact for bracketed dependency id…
vitali87 Aug 30, 2026
99a4321
fix: scale the DP chunker's same-file boundary penalty with the token…
vitali87 Aug 30, 2026
be95d76
fix: keep PR bodies under GitHub's 65,536-character limit
vitali87 Aug 30, 2026
e3e6755
fix: validate a custom PR template before any branch is created
vitali87 Aug 30, 2026
a4bdb68
fix: report a missing gh or git binary as a clean error instead of a …
vitali87 Aug 30, 2026
2745863
fix: retarget a stacked child PR at the plan base before merging it
vitali87 Aug 30, 2026
6ee6e31
fix: stage files the dev branch tracks despite .gitignore when commit…
vitali87 Aug 30, 2026
97cc59d
fix: reject submodule pointer changes up front instead of failing at …
vitali87 Aug 30, 2026
533e044
fix: reject non-http webhook URLs for merge --notify up front
vitali87 Aug 30, 2026
9332e8c
fix: do not run repository hooks when creating sub-PR worktrees
vitali87 Aug 30, 2026
ef3cd90
fix: report a redirected webhook URL instead of silently posting nothing
vitali87 Aug 30, 2026
df4230e
fix: check gh authentication for the target host only
vitali87 Aug 30, 2026
2df034b
fix: treat a bare PR number as a PR even when git resolves it as an a…
vitali87 Aug 30, 2026
72fb0aa
fix: prune stale tracking refs before pushing so re-split branches ar…
vitali87 Aug 30, 2026
e2efecd
fix: let split --dry-run run unattended instead of aborting on closed…
vitali87 Aug 30, 2026
f4524d6
Merge origin/main into fix/openai-failed-status
claude Sep 27, 2026
b324484
Merge origin/main into fix/show-assignment-type-markup
claude Sep 27, 2026
77a17cc
Merge origin/main into fix-stale-worktree-entries
claude Sep 27, 2026
e8a1c7b
Merge origin/main into fix/validate-settings-before-cleanup
claude Sep 27, 2026
37fa4ad
Merge origin/main into fix/split-dry-run-preamble
claude Sep 27, 2026
5e19445
Merge origin/main into fix/merge-closed-while-polling
claude Sep 27, 2026
f6974c9
Merge origin/main into fix/truncation-warning-scope
claude Sep 27, 2026
069a82d
Merge origin/main into fix/escape-plan-text-in-rich-output
claude Sep 27, 2026
e49d96f
Merge origin/main into fix/pr-creation-in-dag-order
claude Sep 27, 2026
8bcca1d
Merge origin/main into fix/worktree-add-without-hooks
claude Sep 27, 2026
2d04179
Merge fix/openai-failed-status into fix/truncation-warning-scope
claude Sep 27, 2026
c6f5dca
Merge origin/main into fix/stacked-merge-retargets-base
claude Sep 27, 2026
e8742cc
Merge origin/main into fix/add-ignored-paths-with-force
claude Sep 27, 2026
3fd8e57
Merge origin/main into fix/cp-sat-timeout-bound
claude Sep 27, 2026
6126b95
Merge origin/main into fix/pr-body-length-cap
claude Sep 27, 2026
9437e7b
docs: describe the pr_closed exit for a PR closed during the auto-mer…
claude Sep 27, 2026
e830066
Merge fix/merge-closed-while-polling into fix/stacked-merge-retargets…
claude Sep 27, 2026
64a0751
Merge fix/escape-plan-text-in-rich-output (with main) into fix/escape…
claude Sep 27, 2026
67ff5ac
Merge origin/main into fix/dry-run-non-interactive
claude Sep 27, 2026
dc243a9
Merge origin/main into fix/cp-sat-timeout-message
claude Sep 27, 2026
2301d41
Merge origin/main into fix/min-loc-in-llm-prompt
claude Sep 27, 2026
cd69c24
Merge origin/main into fix/coverage-overlap-wording
claude Sep 27, 2026
0a2dd29
Merge origin/main into fix/reject-submodule-changes
claude Sep 27, 2026
32e0a62
Merge origin/main into fix/empty-diff-refused
claude Sep 27, 2026
f9292bd
Merge origin/main into fix/notify-url-scheme
claude Sep 27, 2026
d109d1c
Merge origin/main into fix/validate-pr-template-early
claude Sep 27, 2026
deb20f3
Merge origin/main into fix/dp-chunk-file-boundary-penalty
claude Sep 27, 2026
c4600b0
Merge fix/pr-body-length-cap into fix/validate-pr-template-early
claude Sep 27, 2026
568ccea
Merge origin/fix/validate-settings-before-cleanup into fix/pr-number-…
claude Sep 27, 2026
2c0fd0e
Merge origin/main into fix/prune-stale-remote-refs
claude Sep 27, 2026
92f6b0b
Merge origin/main into fix/webhook-redirects
claude Sep 27, 2026
b9251f5
Place _is_fork_ref before _split_settings to match the stacked PRs
claude Sep 27, 2026
d859955
Merge origin/main into fix/score-pr-table-escaping
claude Sep 27, 2026
674ceb5
Merge origin/main into fix/gh-auth-status-hostname
claude Sep 27, 2026
aaccf31
fix: use a private temp dir for the no-hooks hooksPath
claude Sep 27, 2026
8a7f17e
Merge origin/main into fix/emptied-file-not-deleted
claude Sep 27, 2026
f2cf954
Merge origin/main into fix/dag-render-multi-parent
claude Sep 27, 2026
98d2004
Merge origin/main into fix/missing-cli-tools-error
claude Sep 27, 2026
4b4bab2
Merge origin/main into fix/merge-fetch-error-exit
claude Sep 27, 2026
a0d19ba
Merge fix/emptied-file-not-deleted (with origin/main) into fix/apply-…
claude Sep 27, 2026
75b2792
Merge origin/main into fix/force-fetch-fork-refs
claude Sep 27, 2026
8d016ba
Merge fix/apply-file-modes (with origin/main) into fix/materialize-sy…
claude Sep 27, 2026
d91f0e9
Merge origin/main into fix/chunk-summary-hunk-indices
claude Sep 27, 2026
d99ce23
Merge origin/main into fix/score-comment-stale
claude Sep 27, 2026
7e54091
Merge origin/main into fix/split-namespace-collisions
claude Sep 27, 2026
2bba5bd
Merge origin/main into fix/reject-mode-only-changes
claude Sep 27, 2026
fc5b24d
Merge origin/main into fix/score-pr-ref-clobbering
claude Sep 27, 2026
46e809d
Merge origin/main into docs/action-example-pin
claude Sep 27, 2026
a269965
Merge origin/main into docs/action-objective-output
claude Sep 27, 2026
fb4377d
Merge origin/main into docs/action-permissions
claude Sep 27, 2026
953f6d8
Merge remote-tracking branch 'origin/fix/coverage-overlap-wording' in…
claude Sep 27, 2026
e8ea2ef
Merge remote-tracking branch 'origin/fix-stale-worktree-entries' into…
claude Sep 27, 2026
16855cb
Merge remote-tracking branch 'origin/fix/reject-submodule-changes' in…
claude Sep 27, 2026
7ce66a1
Merge remote-tracking branch 'origin/fix/prune-stale-remote-refs' int…
claude Sep 27, 2026
681e22e
Merge remote-tracking branch 'origin/fix/truncation-warning-scope' in…
claude Sep 27, 2026
0efeaeb
Merge remote-tracking branch 'origin/fix/cp-sat-timeout-bound' into c…
claude Sep 27, 2026
1a9433d
Merge remote-tracking branch 'origin/fix/cp-sat-timeout-message' into…
claude Sep 27, 2026
de6336c
Merge remote-tracking branch 'origin/fix/min-loc-in-llm-prompt' into …
claude Sep 27, 2026
d315cae
Merge remote-tracking branch 'origin/fix/dp-chunk-file-boundary-penal…
claude Sep 27, 2026
4646467
Merge remote-tracking branch 'origin/fix/score-pr-table-escaping' int…
claude Sep 27, 2026
9a3336f
Merge remote-tracking branch 'origin/fix/split-dry-run-preamble' into…
claude Sep 27, 2026
be572d0
Merge remote-tracking branch 'origin/fix/dry-run-non-interactive' int…
claude Sep 27, 2026
5f507e1
Merge remote-tracking branch 'origin/fix/gh-auth-status-hostname' int…
claude Sep 27, 2026
3b0d53f
Merge remote-tracking branch 'origin/fix/worktree-add-without-hooks' …
claude Sep 27, 2026
00b1af3
Merge remote-tracking branch 'origin/fix/webhook-redirects' into clau…
claude Sep 27, 2026
6917420
Merge remote-tracking branch 'origin/fix/empty-diff-refused' into cla…
claude Sep 27, 2026
fce8f5e
Merge remote-tracking branch 'origin/fix/validate-settings-before-cle…
claude Sep 27, 2026
e90e39e
Merge remote-tracking branch 'origin/fix/pr-number-before-rev-parse' …
claude Sep 27, 2026
d5e25ab
Merge remote-tracking branch 'origin/fix/pr-creation-in-dag-order' in…
claude Sep 27, 2026
c4c2808
Merge remote-tracking branch 'origin/fix/validate-pr-template-early' …
claude Sep 27, 2026
0a1391c
Merge remote-tracking branch 'origin/fix/missing-cli-tools-error' int…
claude Sep 27, 2026
6fc785e
Merge remote-tracking branch 'origin/fix/dag-render-multi-parent' int…
claude Sep 27, 2026
a518f12
Merge remote-tracking branch 'origin/fix/force-fetch-fork-refs' into …
claude Sep 27, 2026
9148358
Merge remote-tracking branch 'origin/fix/chunk-summary-hunk-indices' …
claude Sep 27, 2026
93bc6f5
Merge remote-tracking branch 'origin/fix/split-namespace-collisions' …
claude Sep 27, 2026
2f376f7
Merge remote-tracking branch 'origin/fix/materialize-symlinks' into c…
claude Sep 27, 2026
62ab455
Merge remote-tracking branch 'origin/fix/reject-mode-only-changes' in…
claude Sep 27, 2026
bf1d8b8
Merge remote-tracking branch 'origin/fix/merge-fetch-error-exit' into…
claude Sep 27, 2026
a617e1d
Merge remote-tracking branch 'origin/fix/score-comment-stale' into cl…
claude Sep 27, 2026
53a9f3e
Merge remote-tracking branch 'origin/fix/score-pr-ref-clobbering' int…
claude Sep 27, 2026
cc798ad
Merge remote-tracking branch 'origin/docs/action-example-pin' into cl…
claude Sep 27, 2026
9684aaa
Merge remote-tracking branch 'origin/docs/action-objective-output' in…
claude Sep 27, 2026
584fffd
Merge remote-tracking branch 'origin/docs/action-permissions' into cl…
claude Sep 27, 2026
3286cee
Merge remote-tracking branch 'origin/fix/add-ignored-paths-with-force…
claude Sep 27, 2026
041bc1d
Merge remote-tracking branch 'origin/fix/show-assignment-type-markup'…
claude Sep 27, 2026
0347578
fix: read the per-branch plan file in the score script
claude Sep 27, 2026
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
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ Works for same-repo and fork PRs: the PR's head (`refs/pull/<N>/head`) is fetche
pr-split split '#42' --base main
```

A bare number (with or without `#`) is always treated as a PR number, even if a
local branch or abbreviated commit hash with the same digits exists. To split a
local branch literally named like a number, pass its full ref:

```bash
pr-split split refs/heads/1006 --base main
```

### Split a fork PR by user:branch

```bash
Expand Down Expand Up @@ -101,7 +109,7 @@ pr-split split feature-branch --base main --dry-run
pr-split split feature-branch --base main --stack
```

A group with a single parent has its branch cut from the parent group's branch, carries the parent's hunks for shared files, and targets the parent's branch, with or without `--stack`. Its PR shows only its own diff, compiles standalone, and GitHub retargets it automatically as the parent merges. A group with several parents is cut from the merge base, carries every ancestor's changes, and targets the base branch, so its diff also includes those ancestors' changes (its PR description lists them). Groups with no dependencies are cut from the merge base and target the base branch. `--stack` additionally registers the chains as native GitHub stacks.
A group with a single parent has its branch cut from the parent group's branch, carries the parent's hunks for shared files, and targets the parent's branch, with or without `--stack`. Its PR shows only its own diff and compiles standalone. When `pr-split merge` reaches such a child it retargets the PR at the base branch (`gh pr edit --base`) right before merging it, since GitHub only does that itself for native stacks or when the parent's head branch is deleted (which `gh pr merge --auto` skips). A group with several parents is cut from the merge base, carries every ancestor's changes, and targets the base branch, so its diff also includes those ancestors' changes (its PR description lists them). Groups with no dependencies are cut from the merge base and target the base branch. `--stack` additionally registers the chains as native GitHub stacks.

A new file larger than `--max-loc` cannot fit in any one sub-PR. With `--stack` it is cut into pieces between top-level definitions: Python files at top-level statements, and other languages at unindented lines after a blank line, outside brackets, strings and comments. Each piece is appended by a PR stacked on the one holding the piece before it. Every layer holds a valid prefix of the file, and the top of the chain holds the whole file. Without `--stack` a new file is never cut.

Expand All @@ -121,17 +129,17 @@ Shows a table with each sub-PR's ID, title, branch, PR number, live state (OPEN/
pr-split merge
```

Walks the dependency DAG and merges each PR in topological order. Skips already-merged, closed, draft, review-required, or changes-requested PRs, and every PR whose dependency was not merged in this run (independent subtrees still proceed). Stops if a merge fails. Exits 1 whenever a merge failed or any PR was left blocked, so re-run once the blocking PRs are ready.
Walks the dependency DAG and merges each PR in topological order. Each child PR that targets its parent's branch is retargeted at the base branch right before it is merged (native-stack PRs are left to GitHub). Skips already-merged, closed, draft, review-required, or changes-requested PRs, and every PR whose dependency was not merged in this run (independent subtrees still proceed). Stops if a merge fails. Exits 1 whenever a merge failed, any PR was left blocked, or a PR's state could not be fetched from GitHub (check `gh auth status`), so re-run once the cause is resolved.

Use `--auto` to queue merges behind CI checks (uses `gh pr merge --auto`):

```bash
pr-split merge --auto
```

`--auto` is not fire-and-forget: after queueing a batch, `merge` waits for every PR in it to reach `MERGED` before moving on to the dependent batch, polling GitHub every 10 seconds for up to 10 minutes per batch. If a PR is still unmerged when the timeout expires, or gets closed while waiting, the command stops before the dependent batch and exits 1 (webhook `exit_reason: incomplete_batch`); re-run `pr-split merge --auto` once CI has caught up to continue from where it left off.
`--auto` is not fire-and-forget: after queueing a batch, `merge` waits for every PR in it to reach `MERGED` before moving on to the dependent batch, polling GitHub every 10 seconds for up to 10 minutes per batch. If a PR is still unmerged when the timeout expires, the command stops before the dependent batch and exits 1 (webhook `exit_reason: incomplete_batch`). A PR closed while waiting is listed as `<id> (CLOSED)` among the skipped PRs, its dependants are skipped as blocked, and the run exits 1 (webhook `exit_reason: pr_closed`). Either way, re-run `pr-split merge --auto` once CI has caught up to continue from where it left off.

Use `--notify` to POST merge results to a webhook URL (e.g. Slack, Discord):
Use `--notify` to POST merge results to an http(s) webhook URL (e.g. Slack, Discord); other schemes are rejected up front, and a delivery failure (timeout, non-2xx) is logged as a warning without changing the exit code:

```bash
pr-split merge --notify https://hooks.slack.com/...
Expand Down Expand Up @@ -253,7 +261,7 @@ commit_message = "chore: release notes for {title}" # {id}, {title}, {index}

### Re-split with different parameters

Running `split` again when a plan already exists will prompt you to clean up existing branches and PRs before re-planning. Dry-run plans are silently overwritten.
Running `split` again when a plan already exists will prompt you to clean up existing branches and PRs before re-planning. Dry-run plans are silently overwritten. With `--dry-run`, `split` never closes PRs or deletes branches: if the saved plan has already been executed it exits 1 and asks you to run `pr-split clean` first.

### Clean up

Expand Down Expand Up @@ -302,6 +310,7 @@ on:
branches: [main]

permissions:
contents: read
pull-requests: write

jobs:
Expand Down Expand Up @@ -337,7 +346,7 @@ jobs:
|--------|-------------|
| `total-loc` | Total lines of code in the PR diff |
| `total-groups` | Number of suggested groups |
| `objective` | Plan objective score (lower is better) |
| `objective` | Simplified plan score, `overflow × 1000 + file_scatter × 50 + groups` (lower is better; `0` when no split plan was scored; not the full `score_plan` objective described in METHODOLOGY.md) |
| `should-split` | Whether the PR should be split (`true`/`false`) |

## Planning backends
Expand Down
14 changes: 11 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ outputs:
description: "Number of suggested groups"
value: ${{ steps.score.outputs.total_groups }}
objective:
description: "Plan objective score (lower is better)"
description: "Simplified plan score: overflow*1000 + file_scatter*50 + groups (lower is better; 0 when no split plan was scored)"
value: ${{ steps.score.outputs.objective }}
should-split:
description: "Whether the PR should be split (true/false)"
Expand Down Expand Up @@ -79,11 +79,17 @@ runs:
run: python "${{ github.action_path }}/scripts/score_pr.py"

- name: Post comment
if: inputs.post-comment == 'true' && steps.score.outputs.should_split == 'true'
if: inputs.post-comment == 'true' && steps.score.outputs.comment_path != ''
uses: actions/github-script@v7
env:
SHOULD_SPLIT: ${{ steps.score.outputs.should_split }}
with:
script: |
const fs = require('fs');
if (!context.payload.pull_request) {
core.info('Not a pull_request event; no comment to post.');
return;
}
const body = fs.readFileSync('${{ steps.score.outputs.comment_path }}', 'utf8');
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
Expand All @@ -92,13 +98,15 @@ runs:
});
const existing = comments.find(c => c.body.includes('<!-- pr-split-score -->'));
if (existing) {
// Always refresh an existing comment so a PR that shrank below
// the threshold no longer shows a stale split plan.
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
} else if (process.env.SHOULD_SPLIT === 'true') {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
Expand Down
Loading
Loading