Skip to content

feat(merge): remote-aware push with divergence reconciliation (BRW-IJBPWQ) - #1

Closed
JonathanWorks wants to merge 1 commit into
mainfrom
feat/merge-remote-reconcile
Closed

feat(merge): remote-aware push with divergence reconciliation (BRW-IJBPWQ)#1
JonathanWorks wants to merge 1 commit into
mainfrom
feat/merge-remote-reconcile

Conversation

@JonathanWorks

Copy link
Copy Markdown

Lifts the manual `wt-restack` recovery flow into a built-in wt merge phase so agent-only workflows no longer spend ~15 min per merge recovering from the non-fast-forward collision when a feature branch has intermediate CI-visibility commits.

Why

In agent-only workflows (pwm-os, DIRTMAN, brainwrap itself), feature branches are pushed to origin while work is in progress because CI, PR review, and multi-session handoffs all need the commits resolvable on the remote. When `wt merge` later locally squashes the branch, a plain `git push` hits non-fast-forward. Force-push is hook-blocked in governed repos. The workaround was a manual `wt-restack` — create a `-vN` branch from the squash, close the old PR, open a new PR. Two such events in a single pwm-os session on 2026-04-22 (PRs 695→703 and 709→714) motivated this change.

What

A new module `src/commands/worktree/remote_reconcile.rs` classifies local-vs-remote state into four variants and dispatches to a configured reconciliation strategy:

  • `Absent` — first push, normal `-u origin `
  • `InSync` — no-op
  • `Ahead` — rebase local onto remote, push (fast-forward)
  • `Diverges` — apply `[merge] on_diverged_remote`:
    • `RemoteSquash` (default): find/open PR, `gh pr merge --squash --delete-branch`, fetch target. Zero force-pushes; main gets one clean squash commit.
    • `Restack`: create `-vN`, close old PR with supersession comment, open new PR with `Supersedes #N` body. Canonical pwm-os pattern.
    • `Abort`: return a structured error naming the recovery commands. No side effects.

Config

Three new optional fields on `MergeConfig`, all defaulting to the pre-0.38 behavior:

```toml
[merge]

master gate; when false, origin push remains a project pre-merge hook responsibility

push_to_origin = true

default "remote-squash" when unset

on_diverged_remote = "remote-squash"

lets remote-squash auto-open a draft PR if none exists (default true)

auto_open_pr_if_missing = true
```

Invariants preserved

  • No `--force` push in any path. Every reconciliation creates new commits — either server-side via GitHub, or a fresh `-vN` branch locally.
  • `gh pr merge` stays internal. The developer-facing governance rule "never use `gh pr merge`" is unchanged; wt invokes it as an implementation detail, same way it already invokes `git push` and `gh pr create`.
  • Default behavior unchanged. `push_to_origin = false` preserves the pre-0.38 model exactly. Existing projects with `[[pre-merge]] push = "..."` hooks keep working.

Tests

  • 7 new unit tests for pure helpers (PR-number parsing from URL/numeric/trailing-newline, supersession body formatting, abort message content).
  • End-to-end against a live `gh` + `origin` deferred to a follow-up (needs sandbox GitHub repo).
  • `cargo test --lib` passes 1000/1001. One failure (`test_mock_config_write`) is pre-existing on main without this change.
  • `cargo test --bin wt` passes 584/584.
  • `cargo clippy --all-targets -- -D warnings` clean.
  • `cargo fmt` applied.

Rollout plan

  1. Merge this PR.
  2. Tag + build (separate commit for version bump).
  3. Deploy to fleet (grokrlabs + grunkle + beelink + laptop via the existing worktrunk install flow).
  4. Follow-up PR: update `pwm-os/.config/wt.toml` with `[merge] push_to_origin = true` and remove the project-level `push` + `pr` pre-merge hooks.
  5. Follow-up card: end-to-end test harness against a sandbox GitHub repo covering all four `RemoteState` variants.

References

  • BRW-IJBPWQ (WT project, in BUILD) — design doc.
  • pwm-os session 2026-04-22 — two wt-restack events this change automates.

…BPWQ)

Adds a built-in wt merge phase that pushes the feature branch to origin
with divergence-aware reconciliation, replacing the need for a project-
level `[[pre-merge]] push = "git push -u origin HEAD"` hook that fails
non-fast-forward when the remote has intermediate CI-visibility commits.

Problem
-------
In agent-only workflows, feature branches are pushed to origin during
development because CI, PR review, and multi-session handoffs all need
the commits resolvable on the remote. When `wt merge` later locally
squashes that branch, the existing push phase hits non-fast-forward.
Force-push is disallowed by convention in governed repos (pwm-os has
this hook-blocked unconditionally).

The previous workaround was a manual `wt-restack` dance — create a
`-vN` branch from the squash, close the old PR, open a new PR — costing
~15 minutes per merge. This change lifts that flow into wt itself.

Mechanism
---------
New module `src/commands/worktree/remote_reconcile.rs`:
- `RemoteState` — snapshot of local-vs-remote (Absent / InSync /
  Diverges{behind,ahead} / Ahead), produced by
  `classify_remote_state(repo, branch)`.
- `reconcile_and_push(repo, branch, target, strategy, auto_open_pr)`
  dispatches on state. Absent → first push. InSync → no-op. Ahead →
  rebase local onto remote, push (fast-forward). Diverges → apply the
  configured `RemoteDivergenceStrategy`:
    * `RemoteSquash` — find/open PR, `gh pr merge --squash
      --delete-branch`, fetch target to local. Zero force-pushes.
    * `Restack` — create `<branch>-vN`, push, close old PR with
      supersession note, open new PR with "Supersedes #N" body.
    * `Abort` — return a structured error naming recovery commands.

Config (`src/config/user/sections.rs`):
- `RemoteDivergenceStrategy` enum: `RemoteSquash | Restack | Abort`.
- `MergeConfig` gains three optional fields, all defaulting to the
  pre-0.38 behavior (off):
    * `push_to_origin: Option<bool>` (default false) — master gate.
    * `on_diverged_remote: Option<RemoteDivergenceStrategy>`
      (default `RemoteSquash`).
    * `auto_open_pr_if_missing: Option<bool>` (default true) — lets
      `RemoteSquash` auto-open a draft PR when one doesn't exist.
- Exported through `worktrunk::config::RemoteDivergenceStrategy`.
- `Merge` impl updated to layer the new fields.

Wiring (`src/commands/merge.rs`):
- After rebase, before pre-merge hooks: if `push_to_origin = true`,
  call `reconcile_and_push` and log the outcome. When the outcome is
  `RemoteSquashed`, return Ok early — GitHub has already merged the
  feature into target and deleted the branch; the local merge phase
  would find nothing to do.

Invariants preserved
--------------------
- No `--force` push in any path. Every reconciliation creates new
  commits (server-side via GitHub, or a fresh `-vN` branch locally)
  rather than rewriting shared history.
- `gh pr merge` is only invoked from inside wt; the developer-facing
  "never use gh pr merge" governance rule is unchanged.
- Default behavior unchanged: `push_to_origin = false` preserves the
  pre-0.38 project-level push-hook model exactly.

Tests
-----
Seven new unit tests for pure helpers (PR-number parsing, supersession
body formatting, abort message content). End-to-end tests against a
live gh + origin are tracked as a follow-up; today's unit coverage is
sufficient to catch regressions in the pure logic.

Full `cargo test --lib` passes except one pre-existing unrelated
mock_commands flake (reproduces on main without any changes). Full
`cargo test --bin wt` passes 584/584. `cargo clippy --all-targets
-- -D warnings` clean. `cargo fmt` applied.

References
----------
- brainwrap card BRW-IJBPWQ (WT project) — design doc
- pwm-os sessions 2026-04-22 PR max-sixty#695max-sixty#703 and max-sixty#709max-sixty#714 — two manual
  wt-restack events that this change automates.
@JonathanWorks

Copy link
Copy Markdown
Author

Superseded by replacement branch after local-squash vs remote-history collision — the exact scenario this PR fixes. Recreated on feat/merge-remote-reconcile-v2 with identical tree content + one squash commit. New PR #2 incoming.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant