allod change record: resolve a concurrent push on a default branch #124

Open
opened 2026-07-28 01:21:16 +01:00 by allod-agent · 0 comments
Member

When two agents commit in place to the same repo's default branch, the second one's push is rejected as non-fast-forward and recovery is manual. This tracks making allod change record resolve that itself — and, first, the criterion for deciding whether it is worth building at all. It is split out of allod/tools#118 so the two concurrency refusals there can land without waiting on it.

Part of allod/tools#115.

Primary goals:

  • Resolve the rejection in place — on a non-fast-forward rejection pushing a repo's default branch, rebase onto the updated remote and retry, so two agents appending to memory or planning docs do not need manual recovery.
  • Force the fail-closed config rather than inherit it — pass -c rebase.autoStash=false explicitly, because with autostash enabled git stashes the other agent's uncommitted work, rebases, and unstashes it, which is the one genuinely dangerous path here.
  • Abort on conflict; never leave a half-state — a rebase conflict must git rebase --abort and report the original rejection, leaving the branch, working tree, and local commit exactly as they were.
  • Decide before building — the criterion below governs whether this is implemented or closed as not planned.

Verified behavior

Measured against a two-clone fixture rather than reasoned about, because the safety of this change rests entirely on what git does when a second agent is active in the same tree:

Situation What git does Consequence
Another agent has uncommitted work in the shared tree git pull --rebase refuses — cannot pull with rebase: You have unstaged changes Fails closed, touches nothing, the other agent's edit survives intact
Tree clean, both agents appended to the same file Conflict: rebase-in-progress, HEAD detached, conflict markers written into the shared working tree Recoverable in full by git rebase --abort

The first row is the important one: git's own dirty-tree guard is what prevents this from reintroducing the collision class allod/tools#115 exists to close, so it is not a hazard that needs new machinery — but it holds only while rebase.autoStash is false. This VM sets pull.rebase true and leaves autostash unset, so the property holds today by default and would stop holding silently if that config changed. Hence the explicit -c.

Why this is deferred rather than dropped

The failure is already loud: allod runs set -euo pipefail, so the rejected push at allod:369 exits non-zero on its own. The recovery is already documented in allod/memory git-workflow.md as one command, git pull --rebase then push again. So the whole value of this change is saving one command in one flow, against the cost of a rebase state machine with a forced-config requirement and a conflict-abort path that needs its own tests.

Architecture principle 15 puts minimalism first and principle 14 says gates that cannot demonstrate their worth get deleted rather than elaborated. Neither says never — they say show the value first.

Decision criterion

Implement when a rejected push in the in-place default-branch flow is observed to cost real recovery cycles rather than being a rare event an agent handles in one command. That becomes measurable once allod/tools#118's guards land and the in-place flow is the only shared-checkout flow left, since allod/tools#116 moves every branch change into its own worktree. If the rejection turns out to be rare at that point, close this as not planned and keep the documented manual recovery.

Scope

In: the push step of allod change record (allod:369), and tests covering a rejection that rebases and retries cleanly, a rejection that conflicts and aborts, and a rejection while another agent has uncommitted work in the tree. Out: the moved-checkout and swept-edit refusals, which stay in allod/tools#118; worktree creation policy, which is allod/tools#116.

When two agents commit in place to the same repo's default branch, the second one's push is rejected as non-fast-forward and recovery is manual. This tracks making `allod change record` resolve that itself — and, first, the criterion for deciding whether it is worth building at all. It is split out of allod/tools#118 so the two concurrency refusals there can land without waiting on it. Part of allod/tools#115. Primary goals: - **Resolve the rejection in place** — on a non-fast-forward rejection pushing a repo's default branch, rebase onto the updated remote and retry, so two agents appending to memory or planning docs do not need manual recovery. - **Force the fail-closed config rather than inherit it** — pass `-c rebase.autoStash=false` explicitly, because with autostash enabled git stashes the *other* agent's uncommitted work, rebases, and unstashes it, which is the one genuinely dangerous path here. - **Abort on conflict; never leave a half-state** — a rebase conflict must `git rebase --abort` and report the original rejection, leaving the branch, working tree, and local commit exactly as they were. - **Decide before building** — the criterion below governs whether this is implemented or closed as not planned. ### Verified behavior Measured against a two-clone fixture rather than reasoned about, because the safety of this change rests entirely on what git does when a second agent is active in the same tree: | Situation | What git does | Consequence | |---|---|---| | Another agent has uncommitted work in the shared tree | `git pull --rebase` refuses — `cannot pull with rebase: You have unstaged changes` | Fails closed, touches nothing, the other agent's edit survives intact | | Tree clean, both agents appended to the same file | Conflict: rebase-in-progress, HEAD detached, conflict markers written into the shared working tree | Recoverable in full by `git rebase --abort` | The first row is the important one: git's own dirty-tree guard is what prevents this from reintroducing the collision class allod/tools#115 exists to close, so it is not a hazard that needs new machinery — but it holds only while `rebase.autoStash` is false. This VM sets `pull.rebase true` and leaves autostash unset, so the property holds today by default and would stop holding silently if that config changed. Hence the explicit `-c`. ### Why this is deferred rather than dropped The failure is already loud: `allod` runs `set -euo pipefail`, so the rejected push at `allod:369` exits non-zero on its own. The recovery is already documented in `allod/memory` `git-workflow.md` as one command, `git pull --rebase` then push again. So the whole value of this change is saving one command in one flow, against the cost of a rebase state machine with a forced-config requirement and a conflict-abort path that needs its own tests. Architecture principle 15 puts minimalism first and principle 14 says gates that cannot demonstrate their worth get deleted rather than elaborated. Neither says never — they say show the value first. ### Decision criterion Implement when a rejected push in the in-place default-branch flow is observed to cost real recovery cycles rather than being a rare event an agent handles in one command. That becomes measurable once allod/tools#118's guards land and the in-place flow is the only shared-checkout flow left, since allod/tools#116 moves every branch change into its own worktree. If the rejection turns out to be rare at that point, close this as not planned and keep the documented manual recovery. ### Scope In: the push step of `allod change record` (`allod:369`), and tests covering a rejection that rebases and retries cleanly, a rejection that conflicts and aborts, and a rejection while another agent has uncommitted work in the tree. Out: the moved-checkout and swept-edit refusals, which stay in allod/tools#118; worktree creation policy, which is allod/tools#116.
Sign in to join this conversation.
No description provided.