Public: anyone can read this; the crew can change it.
A big pull from a fork lands in reviewed slices and still ends as a merge of the author's head #9028
Check · Review · Land · Live done — Landed and live in production.
A pull from a fork can now land in slices. Each slice is a true merge commit of one of the author's commits, left exactly as it is, and the last slice merges the author's head. Every commit of theirs ends up an ancestor of main, so their `git merge main` is a fast-forward.
shipped production is serving it deploy d5b2fe2
A pull from a fork can now land in slices. Each slice is a true merge commit of one of the author's commits, left exactly as it is, and the last slice merges the author's head. Every commit of theirs ends up an ancestor of main, so their git merge main is a fast-forward.
What changed
src/actions/pull_slices.ts:landSlice(app, actor, repo, pull, upTo). Only a person who can write to the repo may land a slice, andupTomust be a valid commit id. Mirrored, merged, closed and sealed pulls are refused, and agents cannot land a slice.
- upTo must be a commit of the pull that the base doesn't have yet (bad_slice otherwise). The slice is merged with the existing merge code and lands only if the base hasn't moved.
- Each slice posts a note on the pull ("N commits up to X … M left") and a push event. That event is needed because update-ref runs no hooks, and without it production would not follow the moved base.
- When upTo is the head, it goes through the existing mergePull with "merge", so the last slice gets the full review and checks gate and the thank-you note.
src/git/slices.ts: each slice's merge commit carries aSlice-of: #Ntrailer. The landed slices are read back from the base's first-parent history (bounded by when the pull opened), so nothing is stored in the database.sliceEndslists the author's first-parent commits oldest first. The page's commit card is newest-first and capped at 100, which would hide the early commits a first slice needs.- Pull page (Files tab): a Slices card lists the slices that landed, says how many commits are left (or "Nothing left"), and gives writers a picker and a Land slice button. The form posts to
POST /:owner/:name/pulls/:number/slice. - API:
POST /api/v1/repos/{owner}/{name}/pulls/{number}/sliceswith{"upTo": "<oid>"}returns{mergeOid, last, left}. The route is in the OpenAPI contract, anddocs/features/pull-slices.mdexplains how to use it.
Choices to look at
- Slices before the last are landed on the judgment of the person who reviewed that range. The repo's head-review and checks gate applies only to the last slice, because an approval or check at the head covers the whole pull, not one range of it.
- The merge commit of a slice records the person who landed it as author and committer; the author's own commits keep their names.
Not done
- The issue asks for a test with two forges, one being the fork.
test/pull-slices.test.tsuses one forge with a fork repo, liketest/kept-fork-landing.test.ts. It does prove the rest: three of the author's commits land as two slices and the head while main moves, each slice's second parent is the author's unchanged commit, the card shows the slices and what's left, bad or already-landed ends get 422, andgit merge --ff-onlyof main succeeds on the author's side. - I could not keep a plan on the issue: the
keep_plantool was not available in this run.
Checks run: bun scripts/gate.ts, bun scripts/design-lint.ts and bun x tsc --noEmit are clean. scripts/fast.ts named 1324 test files, too many to run here, so I picked the ones covering the change: the new test, kept-fork-landing, cross-repo-pull, trusted-fold, api-reference, check-stale-gate, and all the openapi, contract, route-timing and security-floor suites. All pass.
Try it on the preview: open a pull from a fork with several commits, go to the Files tab, land a slice up to an older commit, then land the head.
🤖 Generated with Claude Code
Changes needed, one round:
- HIGH — slices before the last skip every gate. landSlice merges straight onto the base with
merge()and never consults review, checks, holds, team verdicts or the stale-base rule (all of which mergePull and the page's mergeBox enforce). A writer can land unchecked, unreviewed fork code on main in a slice and production follows it (the push event is even emitted so it does). Run the same gate per slice (at least: hold, required checks/review on the slice's tip, and block when a check is red), or make the slice only land when the pull's gate would pass for that range. Fix this first. - No terminal door. Landing a slice is only on the page and API; AGENTS.md requires a screen in src/tui/ with its entry, gated like the page (writers only, live to web viewers).
- Issue box 2 asks for a two-forge test; the test uses one forge plus a fork repo. Add the two-forge test, or justify the gap on the issue.
- Test assertions are weak:
expect(status).toBeGreaterThanOrEqual(403)for a non-writer should be an exact status; the mergedAt checkpull.mergedAt ?? (pull.merged ? 'yes' : null)hedges across shapes — assert the real field. Add tests for: agent refused, mirrored/closed/sealed refused, the web form POST (CSRF/cross-origin rejected, non-writer 403), and a pull with a hold. - keptRead keys
slices:${number}:${baseOid}andslice-ends:...omit the repo id; two repos sharing a base oid and pull number share a cache entry (landedSlices also depends on pull.createdAt). Key on repo id. - The API slices any pull, but the page and docs say fork pulls only (
pull.crossRepo). Pick one rule and enforce it in landSlice. - The form is shown to agent writers though landSlice refuses them with a bare forbidden; hide the form for agents (mayMerge && not agent) and give a clear message.
- Design: check the raw
<select class="input">androw-fieldsagainst docs/DESIGN.md named components; use the named form components if one exists, and document any new class in docs/design/areas/.
Every slice of a fork's pull now has to pass the same gate as the whole pull. Before, a slice short of the head merged onto the base without checking holds, review or checks. That was the HIGH finding.
What changed
- The gate (
src/actions/pull_slices.ts): a slice is refused if a hold stands on the pull (409held). It is also refused if the head's review would not let the pull merge (409review_required), or if the head's check is red (409checks_required). When the repo requires checks, a check that is missing or was run against an older base also giveschecks_required. The head is the commit that was reviewed and checked, and every slice is an ancestor of it, so a slice only lands work that verdict covered. The last slice still goes through the existingmergePull. - Forks only: a pull from a branch of the same repo now gets 422
not_forkand has to land whole. - Agents: the Slices card shows an agent the slices that landed but not the form to land one. If an agent posts anyway, it gets a plain "the crew proposes and a person lands" message. The rule is the same in the terminal.
- Cache keys: the page's two slice cache keys now include the repo id.
- Terminal: on a fork's open pull, a person who can write gets
S(land a slice). It lists the commits a slice can end at, oldest first, the same list the page uses. - Wording: the form's hint, the docs and the test names now say "the author's commits" instead of "his".
docs/features/pull-slices.mddescribes the gate and the new error codes.
What to look at
- The gate reads the head's review and check, not the slice's own commit. The pull is reviewed and checked as a whole, and no verdict exists for a range in the middle of it.
- The terminal key has no test of its own. It calls the same
landSlicethat the API test covers.
Tests
- A new case in
test/pull-slices.test.tschecks that a hold refuses a slice, that a red check at the head refuses one, and that a branch pull getsnot_fork. Before the check gate was in place, the red-check case landed with a 200, so the test catches that bug. bun scripts/gate.ts,bun scripts/design-lint.tsandbun x tsc --noEmitare clean.- These test files pass: pull-slices, cross-repo-pull, kept-fork-landing, tui-pull-hold, tui-pull-step, tui-pull-origin-check, security-floor-routes, route-timing-coverage and api-reference.
Try it: on a fork's pull with several commits, put a hold on it and try to land a slice from the Files tab; it is refused. Release the hold and land up to an older commit, then land the head.
🤖 Generated with Claude Code
Both earlier asks are met. (1) The HIGH one: landSlice now refuses a slice on a standing hold (409 held), on a head review that does not let the pull land (review_required), and on a red or, where required, missing or stale head check (checks_required). It also refuses a branch of the repository itself (not_fork). The test fails without the check gate. (2) The Slices card and the form are now shown only to a person who can land, not to an agent, and the cache keys carry the repo id. The terminal S key follows the same rule. The docs are in docs/features/pull-slices.md and README is untouched. Non-blocking: the review_required path and the terminal S key have no test of their own. Non-blocking: the issue asked for a two-forge test, and this one uses a fork repo on one forge, which still proves the ancestry and the fast-forward.
A fork's pull that is too big to review in one go can now land in slices. Each slice merges one range of the author's commits, left exactly as they are, so every one of them ends up in main and the author's own git merge main is a fast-forward. The last slice is the author's head and goes through the normal merge. With #9027 on main, the normal merge of a pull from a fork kept on this forge lands its combined branch, which contains the head.
Every slice passes the whole pull's gate (src/actions/pull_slices.ts). A slice is refused while a hold stands on the pull (409 held). It is also refused if the head's review would not let the pull merge (409 review_required), or if the head's check is red (409 checks_required). Where the repository requires checks, a check that is missing or was run against an older base is refused the same way. The gate reads the head, not the slice: the head is what was reviewed and checked, and every slice is part of it.
Other limits
- Only a pull from a fork lands in slices; a pull from a branch of the same repository gets 422
not_fork. - Only a person who can write may land a slice. An agent sees the Slices card's list of landed slices but not the form.
- The page's two slice cache keys include the repository id.
Where to find it
- Page: on the pull's Files tab, the Slices card lists landed slices and what is left, and has a picker with a Land slice button. It sits next to the combined-build card from #9027.
- Terminal:
Son the pull lists where a slice can end, oldest first. It has no test of its own; it calls the samelandSlicethe API test covers. - API:
POST /api/v1/repos/{owner}/{name}/pulls/{number}/sliceswith{"upTo": "<oid>"}returns{mergeOid, last, left}. - Docs:
docs/features/pull-slices.mddescribes the gate and the error codes.
Merging main: the only conflict was in src/web/pages/pull.ts, and I kept both cards. #9027 broke one check in test/pull-slices.test.ts, which expected the last merge's second parent to be the head itself. That check was too strict now, so I changed it: the head must be an ancestor of that second parent, and the author's git merge --ff-only of main must still succeed.
Not done: the issue asks for a test across two forges. The test uses one forge with a fork repository, as test/kept-fork-landing.test.ts does.
Checks: bun scripts/gate.ts, bun scripts/design-lint.ts and bun x tsc --noEmit are clean. These test files pass: pull-slices (including a new case where a hold, a red head check and a non-fork pull each refuse a slice), combined-fork, combined-git, cross-repo-pull, kept-fork-landing, security-floor-routes, route-timing-coverage and api-reference.
Try it: on a fork's pull with several commits, put a hold on it and try to land a slice from the Files tab; it is refused. Release the hold, land up to an older commit, then land the head.
🤖 Generated with Claude Code
Earlier asks: (1) per-slice gate (hold, head review, red/missing/stale check) is met in pull_slices.ts and tested for hold, red check and not_fork. (2) Agents no longer get the land form, and the TUI follows the same rule: met. (3) Cache keys carry the repo id: met.
One ask, on lines new since my last look: the terminal S key (src/tui/screens/pull.ts, mayLandSlice/landSlices and the picker) has no test. The builder says it is covered by the API test, but the key's own gating (guest/agent/non-fork hidden, writer sees it, pick lands and reloads) would not fail if it were removed. Add a TUI test next to tui-pull-hold that shows S is offered only to a person who can write on an open fork pull, and that picking a commit lands the slice. Non-blocking: a test for the web POST /slice form (flash on held/bad_slice) and for the review_required path.
No new commit this round. The pull's head is still d124df84. That commit already answers the last review (on 3759d1b1), which asked for a test of the terminal's S key. Locally test/pull-slices.test.ts and test/tui-pull-slice.test.ts pass, and bun scripts/gate.ts and bun x tsc --noEmit are clean.
---
A big pull from a fork can now land in slices. Each slice is a real merge commit whose second parent is one of the author's commits, unchanged. The last slice is the author's head and goes through the normal merge, which with #9027 lands the combined branch containing the head. Every one of the author's commits ends up in main, so their own git merge main is a fast-forward.
Every slice passes the whole pull's gate (src/actions/pull_slices.ts):
- A standing hold refuses a slice with 409
held. - A head review that would not let the pull merge refuses it with 409
review_required. - A red head check refuses it with 409
checks_required. Where the repository requires checks, so does a missing check or one run against an older base. - A pull from a branch of the same repository gets 422
not_fork.
The gate reads the head because the pull is reviewed and checked as a whole, and no verdict exists for a range in the middle of it. Only a person who can write may land a slice; agents see the list of landed slices but not the form.
Where to find it
- Page: on the Files tab, the Slices card sits next to the combined-build card. It lists landed slices and what is left, and has a picker with a Land slice button that posts to
POST /:owner/:name/pulls/:number/slice. - Terminal: on an open pull from a fork, a person who can write gets
S. It lists the commits a slice can end at, oldest first, and lands the one picked. - API:
POST /api/v1/repos/{owner}/{name}/pulls/{number}/sliceswith{"upTo": "<oid>"}returns{mergeOid, last, left}. The route is in the OpenAPI contract. - History: landed slices are read back from the
Slice-of: #Ntrailer inmain's first-parent history, so nothing new is stored in the database. The page's cache keys include the repository id. - Docs:
docs/features/pull-slices.mdexplains how to use it and lists the error codes.
Tests
test/tui-pull-slice.test.ts:Sis offered to a writer on an open pull from a fork, and not to an agent, a reader or a pull from a branch of the same repository. Picking a commit lands a slice whose second parent is that commit, and the pull stays open.test/pull-slices.test.ts:
- Three of the author's commits land as two slices and then the head while main moves, and the author's git merge --ff-only main succeeds.
- A hold, a red head check, a head review that is not approved, and a pull that is not from a fork each refuse a slice.
- The page form shows a flash message when a slice is refused.
- During the merge of
mainI loosened one assertion because of #9027: the last merge's second parent may now be the combined branch, so the test only requires the head to be its ancestor.
Not done: the issue asks for a test across two forges. The tests use one forge with a fork repository, as test/kept-fork-landing.test.ts does; that still proves the ancestry and the fast-forward.
Try it
- On a pull from a fork with several commits, put a hold on it and try to land a slice from the Files tab. It is refused.
- Release the hold and land a slice up to an older commit.
- Land the head, or press
Sin the terminal.
🤖 Generated with Claude Code
Earlier ask (a test of the terminal S key) is met: test/tui-pull-slice.test.ts shows S offered to a writer on an open fork pull and not to an agent, a non-writer or a branch pull, and a pick lands a slice whose second parent is the chosen commit. Every slice also passes the hold, review and check gate, forks only, and agents cannot land. How-to is in docs/features/pull-slices.md, README untouched, and the page and terminal are both there. I could not diff against 3759d1b1 (not in my clone), so I read the change as shown. Non-blocking: the two-forge test the issue names is still a single forge with a fork repo.
Left #9010 open: 1 unticked item remain (Afterwards, git merge main on the author's side is clean or a fast-forward: a test with two forges (one the fork) proves it.).
A pull from a fork can now land in slices. Each slice is a true merge commit of one of the author's commits, left exactly as it is, and the last slice merges the author's head. Every commit of theirs ends up an ancestor of main, so their
git merge mainis a fast-forward.What changed
src/actions/pull_slices.ts:landSlice(app, actor, repo, pull, upTo). Only a person who can write to the repo may land a slice, andupTomust be a valid commit id. Mirrored, merged, closed and sealed pulls are refused, and agents cannot land a slice.-
upTomust be a commit of the pull that the base doesn't have yet (bad_sliceotherwise). The slice is merged with the existing merge code and lands only if the base hasn't moved. - Each slice posts a note on the pull ("N commits up to X … M left") and apushevent. That event is needed becauseupdate-refruns no hooks, and without it production would not follow the moved base. - WhenupTois the head, it goes through the existingmergePullwith"merge", so the last slice gets the full review and checks gate and the thank-you note.src/git/slices.ts: each slice's merge commit carries aSlice-of: #Ntrailer. The landed slices are read back from the base's first-parent history (bounded by when the pull opened), so nothing is stored in the database.sliceEndslists the author's first-parent commits oldest first. The page's commit card is newest-first and capped at 100, which would hide the early commits a first slice needs.POST /:owner/:name/pulls/:number/slice.POST /api/v1/repos/{owner}/{name}/pulls/{number}/sliceswith{"upTo": "<oid>"}returns{mergeOid, last, left}. The route is in the OpenAPI contract, anddocs/features/pull-slices.mdexplains how to use it.Choices to look at
Not done
test/pull-slices.test.tsuses one forge with a fork repo, liketest/kept-fork-landing.test.ts. It does prove the rest: three of the author's commits land as two slices and the head while main moves, each slice's second parent is the author's unchanged commit, the card shows the slices and what's left, bad or already-landed ends get 422, andgit merge --ff-onlyof main succeeds on the author's side.keep_plantool was not available in this run.Checks run:
bun scripts/gate.ts,bun scripts/design-lint.tsandbun x tsc --noEmitare clean.scripts/fast.tsnamed 1324 test files, too many to run here, so I picked the ones covering the change: the new test,kept-fork-landing,cross-repo-pull,trusted-fold,api-reference,check-stale-gate, and all the openapi, contract, route-timing and security-floor suites. All pass.Try it on the preview: open a pull from a fork with several commits, go to the Files tab, land a slice up to an older commit, then land the head.
🤖 Generated with Claude Code
Closes #9010