From 012-jj-and-the-merge-pipeline.md's task table (design 012).
- Real dependency: T8
- Note: Aedan's reversal: confusing for hand-stacked PRs today, may get way easier with gh stacks
My preference is we find a way to use github stacked PRs, as a prereq to building a proper dev.py rebase command.
So I would like us to dig into why stacked PRs are incompatible with our merge ritual, I find this a bit tricky to believe it's not possible to come up with some decent options to get us the best of both worlds.
My preference is we find a way to use github stacked PRs, as a prereq to building a proper dev.py rebase command.
So I would like us to dig into why stacked PRs are incompatible with our merge ritual, I find this a bit tricky to believe it's not possible to come up with some decent options to get us the best of both worlds.
My preference is we find a way to use github stacked PRs, as a prereq to building a proper dev.py rebase command.
So I would like us to dig into why stacked PRs are incompatible with our merge ritual, I find this a bit tricky to believe it's not possible to come up with some decent options to get us the best of both worlds.
Spec round ran on Aedan's comment above (routed via /spec). Outcome:
The prescriptive card went to nf-oqmx — the already-filed, unclaimed discovery covering exactly this ("merge_pr.py cannot merge any native-stack PR member"), now retitled imperative and carrying the full spec: https://nfold.dev/issue/nf-oqmx
**Why stacks and the merge ritual conflict today — the dig Aedan asked for,
short version:** it is not fundamental. (1) merge_pr.py's unconditional
child-retarget 422s on stack members because GitHub's cascade owns
retargeting; (2) Gh.merge uses the synchronous merge endpoint, which
GitHub refuses for stack members — the async stack-merge REST API
(POST …/pulls/{n}/merge-async + poll) is the sanctioned path, and it is
what gh stack merge itself calls; (3) checks/approvals vanish on the
cascade only because the cascade mints new shas — the sha-staleness class
design 012 R1's contribution-id carry (T3/T4, landed) already re-certifies
for free. All three close with one bounded merge_pr.py extension,
specced on nf-oqmx.
Resequencing, per Aedan's ask ("as a prereq"): this issue (T9,
dev.py rebase) now depends on nf-oqmx in bd. Once nf-oqmx lands and a
second real stack runs clean, T9's shape gets re-read — likely the thin
gh stack rebase wrapper plus the single-branch replay core (design 012 §8
T9 shape (a)/(c)), rather than the full 006a-T2 restack the T8 verdict
provisionally named while stacks were unmergeable.
Both issues remain unclaimed; /triage activates when ready.
Live rehearsal complete — the missing verification this task's shape decision was waiting on. Full detail on bd nf-oqmx (two scratch stacks, round 2 clean): merge_stack() lands a real GitHub-native stack end-to-end through every gate (ledger, presubmit-at-head, authority, UI probe gate), and design 012 R1's T3/T4 carry restamps for free on a genuine cascade-minted head — confirmed live, not just in unit-test fakes:
#1217: delta re-verified (10ef6e77... -> c723d03, content identical) — no review spawned ✓ pure rebase — the existing verdict was restamped for the new head, no re-review needed
Per the spec round's resequencing: T9's shape is now settled as (a)+(c) — single-branch replay is the common core (fetch origin, identify the branch's own commits, replay onto fresh base, refuse loudly on real conflicts, delegate to push with a lease), wrapped by a thin dispatch to gh stack rebase when the branch is part of a GitHub-native stack. NOT the full 006a-T2 refusal-guarded restack T8's verdict provisionally named — that shape was contingent on stacks being unmergeable, which nf-oqmx just closed. Building this now in tools/nflib/devcli.py.
dev.py rebase is up: https://github.com/cfsh/ninefold/pull/1220 — shape (a)+(c) per the live rehearsal (bd nf-oqmx), requesting aedanpope's review (GOVERNANCE-perimeter, tools/nflib/*, cannot self-merge on approval:auto).
Live rehearsal of dev.py rebase itself — the "Done when" criterion (real branch, real rebase, real conflict aborted and retried), which the reviewer on PR #1220 correctly flagged as missing (the merge_stack() rehearsal quoted earlier is a rehearsal of a DIFFERENT thing).
Setup, two independent branches off the same origin/main tip:
trial/rebase-live-mainside: a real, useful edit tightening the wording of games/hello_world/probes/smoke-hello-world.json's _intent line. Opened as PR #1221, reviewed to PASS/approval:auto, merged normally (plain single-PR path).trial/rebase-live-conflict: a local-only (never pushed) edit to the SAME _intent line, deliberately different text — so once #1221 landed on main, replaying this branch onto fresh main would hit a real two-sided conflict on that exact line. (Ran ./dev.py rebase here via a temp worktree carrying this branch's own PR-750.9 code overlaid uncommitted — the trial branch itself predates T9 — a harness detail, not part of the rehearsal's substance.)1. First attempt — real conflict, stops loudly:
$ ./dev.py rebase
dev.py rebase: replaying trial/rebase-live-conflict onto origin/main (no open PR — assuming this was cut from main).
$ git rebase origin/main
$ git status --porcelain
dev.py rebase: stopped on a real conflict —
games/hello_world/probes/smoke-hello-world.json
resolve it, git add the fixed files, then:
./dev.py rebase --continue
or give up and restore the branch exactly as it was:
./dev.py rebase --abort
(exit 3)
2. --abort — clean restore:
$ ./dev.py rebase --abort $ git rebase --abort dev.py rebase: aborted — the branch is back where it was.
git status afterward: clean, git log back to the single original commit — no residue.
3. Reproduced identically — ran ./dev.py rebase again from the restored state; same conflict, same file, same instructions, byte-for-byte.
4. Resolved for real and continued:
$ ./dev.py rebase --continue
$ git rebase --continue
dev.py rebase: conflict resolved — continuing on trial/rebase-live-conflict.
$ ./presubmit.py --all
...
+ dotnet pass 13.8s
+ python pass 116.0s
+ shellcheck pass 0.2s
+ layout pass 3.6s
PASS
$ git push --force-with-lease
$ ./tools/pr/rebuild_f5.py
...
+ f5/integration = main ...
Gated, pushed with a lease, f5 rebuilt — the full delegation to push's own tail, exercised for real, not through fakes.
One honest note: because I resolved the conflict by taking main's wording outright (the scratch edit was throwaway by design), the replayed commit ended up with an EMPTY diff against its new parent — git rebase --continue correctly dropped it rather than creating a no-op commit, so the branch post-rebase is byte-identical to origin/main. That's git rebase's own standard behaviour, not a dev.py rebase bug, and it doesn't weaken what the mechanism proved: the conflict was real, the stop/abort/retry/continue/push/gate sequence all worked exactly as designed.
Scratch branches and their remote refs cleaned up afterward. PR #1220 (the actual dev.py rebase code) has this quoted in reply to the reviewer's GAPS verdict, alongside a notes/graphite.md fix for the same review's other finding.
Holding this one, not assigning — @aedanpope wants to discuss first, since T8's verdict effectively makes this a design amendment rather than a straight build against the T9 card as written.
What changed: the original T9 card (design_docs/012-jj-and-the-merge-pipeline.md §8/§4) was written with the shape genuinely open — "wrapper over gh stack rebase" if the T8 trial adopted native stacks, or "the full 006a-T2 refusal-guarded restack" if it reverted. T8's trial (nf-758.8, GitHub #1117, closed) landed a clear verdict: extend, don't adopt fleet-wide yet — merge_pr.py cannot merge any native-stack PR member today (two hard refusals, detailed in #1117), so stacks and this repo's merge safety model don't currently coexist. The trial's own recommendation for T9: build the full 006a-T2 restack, not the wrapper — that shape was contingent on stacks being adoptable end-to-end including merge, which they're not (yet).
Why this isn't just "follow the verdict and go": the verdict also surfaced a real follow-up need — a human-reviewed merge_pr.py extension onto GitHub's async stack-merge REST path (filed unclaimed at GitHub #1189) — which could eventually flip T9's shape again if it lands. Given that, and that T9 touches the PR-stack ritual devs rely on daily, this reads as a decision worth Aedan's steer before a dev builds it, not a mechanical pickup.
File overlap note for whoever picks this up next: T9 shares notes/merging.md with T6 (nf-758.6, GitHub #1115) — hold until T6 lands to avoid a collision.
Note: this was originally posted as a comment on GitHub issue #1118, which a reconciliation process then closed (stateReason COMPLETED) and redirected to this bd tracking page — mirroring here since bd is the live source of truth and the GitHub comment thread is no longer where this gets read. T9's actual work is NOT complete; only its GitHub mirror issue was closed.
@aedanpope — flagging per your ask; let me know if you want this routed through /spec for a dev to scope, or if you want to just settle the shape here and I'll assign it as-is.