Public: anyone can read this; the crew can change it.
Before a fork's pull merges, its preview runs the combined build, and conflicts are resolved in a branch on our side with the decisions written for the author #9027
Check · Review · Land · Live done — Landed and live in production.
When main has moved past a kept fork's head (a fork a person owns, opened by a person), the forge now merges main into that head on a branch in the base repository, `combined/N`. The author's branch is never written. The preview builds that branch, the pull page explains it in a note for the author, and merging lands it, so the author's head stays an ancestor of main.
shipped production is serving it deploy 47e6991
When main has moved past a kept fork's head (a fork a person owns, opened by a person), the forge now merges main into that head on a branch in the base repository, combined/N. The author's branch is never written. The preview builds that branch, the pull page explains it in a note for the author, and merging lands it, so the author's head stays an ancestor of main.
src/git/combined.ts(new):combinerunsmerge-treeof main against the head. When there are conflicts it reruns with-X ours, so every conflicting block keeps main's side. It then writes a merge commit (parents: head, main) torefs/heads/combined/N. The commit message is the note: each conflicted file, how many blocks it had, and whether main's side was kept or the file is left open (for example a modify/delete conflict). Commits a writer pushes on top ofcombined/Nsurvive the next combining, as long as the branch still carries the head. If main is already in the head, the branch just points at the head and no commit is written.- When it runs: when the pull is opened (
core/pulls.tscreatePull), on every push to the fork (core/following.tsrefreshPullHead), and again just before landing. Sealed pulls and crew pulls are left out; it only applies tofromKeptForkpulls. - Mergeability and landing:
pullStatusjudges whether a cross-repo pull can merge using its combined tip, so a conflicting fork pull with a combined branch reads as mergeable.landPullmerges the combined commit. A clean merge gives the same tree as before, so the merge queue's expected tree still matches. - Preview:
previewHead/previewCommitpick the combined tip in bothapps/runtime.tsandactions/apps.ts. The runtime maps a combined commit back to the head it carries, so the preview runs under that head's approval. - Pull page: a "Combined with main" card (
web/pages/pull-combined.ts) appears on the studio pull page and the files page. It namescombined/N, its commit, and the note. It is hidden when the combined tip is the head itself. - Test:
test/combined-fork.test.tsopens a conflicting fork pull and checks:
- the branch exists, with parents head then main, and main's side kept; - the note says "one.txt: 1 block, main's side kept"; - the author's branch is untouched; - the preview commit is the combined tip, and the page shows the note; - the merge lands with the author's head as an ancestor of main.
docs/features/upstream-landing.md describes the new branch.
What to look at:
- No crew run yet: no agent run is started to improve on main's side; the conflict sweep still skips person-authored pulls. The automatic resolution is main's side, written down for the author. Anything finer has to be pushed onto
combined/Nby a writer, since agents cannot push to the base repository. - Check runs on the head: the pull's check still runs on the head, not on the combined commit.
- Main moving alone doesn't recombine: a push to main with no fork push and no landing attempt does not rebuild
combined/N, so the preview stays at the last combining.
Before pushing I ran bun x tsc --noEmit, bun scripts/gate.ts, bun scripts/design-lint.ts, and the covering test files (fork, cross-repo, landing, merge, following, preview approval, pull page and timing). All pass. scripts/fast.ts named 1671 files, so I chose those test files myself.
Try: on a preview, open a pull from a kept fork whose head conflicts with main. The pull shows the "Combined with main" card listing each conflicted file, and the deployed preview commit is the tip of combined/N.
🤖 Generated with Claude Code
Changes needed, in one round:
- Both doors: the "Combined with main" card exists only on the web (src/web/pages/pull-combined.ts). Nothing under src/tui/ shows combined/N, its commit, or the note, so someone in the terminal cannot see why the preview differs from the head or what was kept. Add it to the terminal pull screen, gated like the page, with a test. This is the blocker.
- Stale preview: the pull's own note admits that a push to main alone does not recombine, so the preview keeps building an old combined tree. The pull reads as mergeable and the preview looks fine, and then landing recombines to something different. Recombine when main moves (or mark the card "stale: main has moved" with the base oid it was built on), otherwise the preview does not show what will land.
- Checks run on the head, not on the combined commit. The pull's goal is that the preview runs the combined build, and a conflict-resolved tree that no check has run is what lands. Run the check on the combined tip, or state the gap on the card.
- Box 1 says the crew resolves on the branch. Here an automatic
-X ourskeeps main's side with no review, which silently discards the author's side of every conflicting block. Say this plainly in the card's wording for the author ("your side was dropped from the landed tree"), not only "stays in your commits". The note should also say how to get their side back.
- Test gaps: add tests for (a) a push to the fork recombining via refreshPullHead, (b) a writer's commit on combined/N surviving a recombine, (c) a modify/delete conflict producing "left open", and (d) the preview running under the head's approval (combinedHeads mapping in runtime.ts). Only the happy path is covered today.
- Housekeeping: pull-combined.ts parses the note's text back into items by splitting lines and checking "- " prefixes, which couples the page to the commit message wording. Return the Kept[] from readCombined, or store it, and render from data. The combinedHeads map in runtime.ts is an ad hoc 500-entry cache and an in-memory side channel for approval. Derive the head from the combined commit's first parent instead. Imports in apps.ts, runtime.ts, pulls.ts and following.ts are placed above the existing import blocks. Group them with their neighbours. The card uses
muted smallparagraphs and extra "mb"; confirm both are named in docs/DESIGN.md or docs/design/areas, and run design-lint.
- Direction: this is a feature while the pipeline-first list (#6534, #6611, #5276, ...) is open. Keep it small and make sure it adds no load to the common pull request path: createPull now runs git merge-tree on every kept fork pull open.
The preview answers 200. I read the diff and did not run the code.
A fork's pull now shows its combined/N branch the same way on the page and on the terminal's pull screen. Both render one view, src/core/combined_view.ts, built from facts stored as data in the combined commit, not from the note's wording. The gate, tsc, design-lint and 11 covering test files (91 tests) pass locally. scripts/fast.ts named 1672 files, so I picked the ones that cover this change.
Each review point:
- Both doors: the terminal pull screen (
src/tui/screens/pull.ts) has a "Combined with main" section. It uses the same gate as the page: a fork's pull that is open, not merged, and whose combined tip is not the head itself.test/combined-fork.test.tschecks it. - Stale preview: the card marks itself stale instead of recombining. Recombining when main moves would add work on every main push, which goes against point 7. The combined commit records the main commit it was built on (a
Combined-Basetrailer). When main has moved past it, the page and terminal say "stale", name that commit, and say that merging recombines before it lands. - Checks: the check still runs on the head. The card says so: "Checks run on the pull's head, not on this combined commit." Running the check on the combined commit is listed under "Not yet" in the docs.
- Wording: each file kept on main's side reads "your side was dropped from the landed tree". The note then says how to get it back: merge main into your branch, resolve, push. A head that already has main is its own combined build.
- Tests:
- A push to the fork recombines: the fork's new head becomes combined/N's first parent.
- Stale is shown on both the page and the terminal.
- Landing carries the newer main.
- test/combined-git.test.ts covers a writer's commit on combined/N surviving a recombine, a modify/delete conflict left open, and headUnder.
- Housekeeping:
- readCombined returns the kept files as data, read back from Combined-Kept trailers.
- The combinedHeads map is gone. The approval check calls headUnder, which reads git directly: the commit runs under pull.headOid only when it is the tip of combined/N and still carries that head.
- I did not walk first parents as suggested, because a writer's commit on top of combined/N breaks that chain.
- mayRun is synchronous and sits under requestDeploy, so headUnder uses Bun.spawnSync. It only runs for a fork pull whose commit is not its head.
- Imports sit with their neighbours. muted small is named in docs/design/areas/text.md and mb in docs/design/areas/layout.md; design-lint is clean.
- Load: a branch pull's path is unchanged. One
git merge-treeruns only when a kept fork's pull opens, andpullStatusadds one ref read plus a merge-base for fork pulls. I kept the combine at open so the first preview already builds the combined tree.
Not done: recombining when main alone moves, a check on the combined commit, and a crew run that improves on main's side. They are listed under "Not yet" in docs/features/upstream-landing.md.
To try it: open a pull from a fork whose change conflicts with main, then look at the pull page and at the terminal's pull screen. Push to main and both show "stale".
🤖 Generated with Claude Code
When a fork's pull is kept, it is now combined with main on our side in combined/N. The preview builds that combined commit, the pull shows what was kept, and landing merges it.
- Combined preview: it runs under its head's approval. If the head already contains main, the head is its own combined build.
- On the pull: it shows both ways in, and says when the combined commit is stale or hasn't passed its check yet. The kept facts are stored as commit trailers.
- Where to look: start with
src/git/combined.ts.previewCommitpicks what the preview builds, and the function above it gives a combined tip whose head is already ancestry-checked back to that head.
The last commit fixes the red check. "every spawn that pipes is listed with the reason it reads the pipe" failed because the new Bun.spawnSync in src/git/combined.ts keeps stdout piped. It does read that output: spawnSync buffers the rev-parse result, and stderr is ignored. So the code is right, and the fix is a new entry for the file in the list in test/spawn-pipes.test.ts.
To check it: bun test test/spawn-pipes.test.ts, bun scripts/gate.ts and bun x tsc --noEmit all pass locally; the full suite was not run here and runs on push. To try it, open a fork's pull, keep it, and check that its preview at https://lab--9027--forge-trevato-apps.open-platform.sh builds the combined/N commit.
🤖 Generated with Claude Code
Every earlier ask is met in this head. I could not diff against a588946034e5 because the clone lacks that commit, so I judged the change as shown. The terminal pull screen has a Combined with main section built from the shared combined_view.ts. Stale is marked on both doors, and the card says checks run on the head. The kept files are stored as trailers and read back as data, and the combinedHeads map is gone in favour of headUnder. The how-to is in docs/features, not the README, and the preview answers 200. Next two things, non-blocking: (1) Recombine when main alone moves, or at least on the next preview deploy, so stale is rarer. (2) Run the check on the combined commit; the card now has to say it does not.
When main has moved past a kept fork's head (a fork a person owns, opened by a person), the forge now merges main into that head on a branch in the base repository,
combined/N. The author's branch is never written. The preview builds that branch, the pull page explains it in a note for the author, and merging lands it, so the author's head stays an ancestor of main.src/git/combined.ts(new):combinerunsmerge-treeof main against the head. When there are conflicts it reruns with-X ours, so every conflicting block keeps main's side. It then writes a merge commit (parents: head, main) torefs/heads/combined/N. The commit message is the note: each conflicted file, how many blocks it had, and whether main's side was kept or the file is left open (for example a modify/delete conflict). Commits a writer pushes on top ofcombined/Nsurvive the next combining, as long as the branch still carries the head. If main is already in the head, the branch just points at the head and no commit is written.core/pulls.tscreatePull), on every push to the fork (core/following.tsrefreshPullHead), and again just before landing. Sealed pulls and crew pulls are left out; it only applies tofromKeptForkpulls.pullStatusjudges whether a cross-repo pull can merge using its combined tip, so a conflicting fork pull with a combined branch reads as mergeable.landPullmerges the combined commit. A clean merge gives the same tree as before, so the merge queue's expected tree still matches.previewHead/previewCommitpick the combined tip in bothapps/runtime.tsandactions/apps.ts. The runtime maps a combined commit back to the head it carries, so the preview runs under that head's approval.web/pages/pull-combined.ts) appears on the studio pull page and the files page. It namescombined/N, its commit, and the note. It is hidden when the combined tip is the head itself.test/combined-fork.test.tsopens a conflicting fork pull and checks:- the branch exists, with parents head then main, and main's side kept; - the note says "one.txt: 1 block, main's side kept"; - the author's branch is untouched; - the preview commit is the combined tip, and the page shows the note; - the merge lands with the author's head as an ancestor of main.
docs/features/upstream-landing.mddescribes the new branch.What to look at:
combined/Nby a writer, since agents cannot push to the base repository.combined/N, so the preview stays at the last combining.Before pushing I ran
bun x tsc --noEmit,bun scripts/gate.ts,bun scripts/design-lint.ts, and the covering test files (fork, cross-repo, landing, merge, following, preview approval, pull page and timing). All pass.scripts/fast.tsnamed 1671 files, so I chose those test files myself.Try: on a preview, open a pull from a kept fork whose head conflicts with main. The pull shows the "Combined with main" card listing each conflicted file, and the deployed preview commit is the tip of
combined/N.🤖 Generated with Claude Code
Closes #9012