# ADR-0059: Branch-and-PR working method — worktrees, merge commits, and reserve-first ADR numbering ## Status Accepted — **2026-06-23**, on branch `chore/dev-workflow` (pending merge). Number **0059**, the second ADR reserved via the reserve-first flow this ADR itself defines (the first was ADR-0058). Updates the **Numbering discipline** section of **ADR-0000** (supersedes its placeholder-until-merge default with reserve-first). Does not affect the subproject ADR namespaces (`docs/website/adr/`, `docs/ci/adr/`). ## Context The repository is now public, and feature work is moving from commit-straight-to-`main` toward branches. A public, multi-branch project needs a written, trackable working method instead of ad-hoc habits. The sharp edge that forced the issue is **ADR numbering**. ADR numbers are a single global integer sequence; two branches that each grab "the next number" collide on merge (this happened, and was the original motivation for ADR-0000's numbering rules). The deeper problem is fundamental: > A contiguous integer requires a single allocator. Plain git branches have > **no shared allocator** until they reconverge at merge — so any number > claimed independently on a branch is speculative and can collide. Earlier work papered over this for two long-lived subprojects by giving them their **own namespaces** (`ADR-website-NNN`, `ADR-ci-NNN`), which is right for a nameable body of decisions but doesn't generalise to every feature branch. ADR-0000 also wrote a **placeholder-until-merge** default ("assign the number at merge"), but it had never actually been exercised, and exercising it revealed two fatal frictions: 1. **ADR numbers are referenced from the first commit onward** — in commit messages (which are immutable under our no-rewrite rule, so a wrong number there is wrong *forever*), in other ADRs, in the PR. A number that only crystallises at merge leaves every earlier reference dangling. 2. ADRs are *not* low-collision in practice: they're born precisely on the **long-lived feature branches most likely to run in parallel**, so "rare collision" is not a safe assumption. So the requirement is a number that is **stable from creation, contiguous, and collision-free** — which is only possible by acquiring a lock *up front*. The insight that unlocks it: **`main` is the registry and an atomic `git push` is the lock** — git serialises ref updates, so a push is a compare-and-swap. We just have to acquire it *before* referencing the number, not at merge. This is a **genuine-choice** area (reasonable practitioners run GitHub Flow, Git Flow, trunk-based, etc.); the decisions below adapt mainstream practice to this repo's constraints (append-only history, self-hosted Gitea, solo-with-subagents development) rather than claiming a canonical default. ## Decision ### D1 — Everything lands via a PR onto a protected `main` `main` is always-green and protected; no direct pushes (one narrow exception — D4). Every change is a branch + pull request onto `main`. One logical change per branch/PR. Branch names carry a conventional prefix (`feat/ fix/ docs/ chore/ refactor/ test/ ci/`) aligned with the commit type. PRs reference their issue (`Closes #N`); the review we already perform (the Devil's-Advocate / `/runda` pass) is pasted into the PR so the public repo carries the audit trail. ### D2 — One worktree per branch (never switch the primary checkout) Each branch is developed in its **own git worktree**, a sibling of the primary checkout named `-worktree-` (the convention already in use for the `website` and `ci` worktrees). Switching the primary checkout's branch while another shell sits in the same directory is how work gets clobbered — worktrees make that impossible. Helpers: - **`scripts/wt-new.sh `** — fetch `main`, create the branch off `origin/main`, and add the sibling worktree; prints its path. - **`scripts/wt-clean.sh [--dry-run]`** — prune stale entries and remove worktrees whose branch is merged into `origin/main` (refuses dirty ones; never touches the primary or current worktree). Run it whenever. ### D3 — Merge commits (`--no-ff`); never rebase or squash Branches land with a **merge commit** (`--no-ff`). This is the only strategy consistent with the project's append-only / no-history-rewrite rule: squash and rebase-merge both rewrite history. Fast-forward-only is out because it would require rebasing branches onto `main`. History gains merge bubbles; that is the accepted cost of never rewriting. ### D4 — Reserve-first ADR numbering via `adr-reserve.sh` ADR numbers are **reserved up front, the moment you know an ADR is needed** — which may be mid-branch or late, so reservation is decoupled from branch creation. `scripts/adr-reserve.sh [title]`: 1. reads an append-only ledger (`docs/adr/RESERVATIONS.log`) plus the existing `NNNN-*.md` ADRs on `main`; next = highest + 1; 2. appends ` `, commits, and **pushes to `main`**; 3. if the push is rejected because `main` moved (someone reserved first), it re-fetches, recomputes, and retries — the push is the compare-and-swap, so allocations are unique by construction, not by luck. It runs from any branch/worktree without touching the working tree (it does the allocation in a throwaway worktree on a detached `origin/main`), and on a *non-race* rejection (e.g. protection misconfigured) it **aborts loudly** rather than spinning. The number is therefore **stable from the moment it returns** — safe to cite in immutable commit messages and other ADRs from the first commit, which is what the references-problem (Context) demands. The ADR file is created as `docs/adr/NNNN-<slug>.md` in the working branch and its README index row is added there as normal work; the ledger line on `main` is the allocation record and never conflicts with feature work (feature branches don't touch it). The ledger may be pruned later; it's a lock record, not state. **Collisions** (two reservations in the same instant) are resolved automatically by the retry; the README index edit is a secondary tripwire if one ever slips through. This **supersedes ADR-0000's placeholder-until-merge default.** Subproject namespaces (`ADR-website-NNN`, `ADR-ci-NNN`) are unchanged — they solve a different problem (a nameable long-lived body of decisions) and stay. ### D5 — Branch protection configuration On `main` (Gitea → Settings → Branches): - **Require pull requests** + **require the CI status check** (`ci / gate*`) to pass before merge, and **block merge of out-of-date branches** (this is what makes "`main` is the lock" airtight for the substantive merges). - **Whitelist the repo owner for direct push.** This is the one narrow carve-out that lets `adr-reserve.sh` push the append-only ledger line directly; everything substantive still goes through a PR. Without it the reserve script aborts with a clear "is the owner whitelisted?" message. ### D6 — Who does what **Pushing and merging are human steps**; automated agents prepare branches, edits, PR text, and commit-message proposals but never push or merge. The reserve script is **run by the human** (so the ledger push originates from the human's invocation, consistent with that rule) — agents may write and test the script but not run its pushing path. ## Forks (all user-chosen, 2026-06-22/23) - **Merge commits (`--no-ff`)** over squash or fast-forward-only — the only option consistent with append-only history (D3). - **Strict everything-via-PR** over allowing trivial direct commits — one carve-out only, the reservation line (D1/D4/D5). - **Branch protection enforced in Gitea** over convention-only — mechanical guarantee for a public repo (D5). - **Reserve-first contiguous integers** over: number-on-merge (rejected — breaks immutable references), Gitea-issue-number ids (rejected — non- contiguous, overlaps the legacy 0001–0057 range), or an allocator bot (rejected — machinery + a PR-rule exemption). Reserve-first keeps the short stable "ADR 0059" reference and needs no new infrastructure beyond one script (D4). - **Worktrees prescribed** over single-checkout branch switching — prevents the same-directory clobber hazard (D2). ## Consequences - A written, public-facing **`CONTRIBUTING.md`** (lean: setup, the CI gate, branch/PR conventions, the user-facing copy rules, "significant changes get an ADR"). The maintainer-facing mechanics (worktrees, the reserve lock, the whitelist) live here and in `CLAUDE.md`, not in CONTRIBUTING. - Three scripts enter `scripts/`: `adr-reserve.sh`, `wt-new.sh`, `wt-clean.sh`, each with a local-`origin` test harness (`test-adr-reserve.sh`, `test-wt.sh`) — the reserve script's allocation, idempotence, race-retry discriminator, and hard-abort paths are all covered (the real-remote push is exercised by the human running it). - `docs/adr/RESERVATIONS.log` appears on `main` as the allocation ledger. - ADR-0000's Numbering-discipline section is updated to point here. - `CLAUDE.md` gains the operational rules an agent must follow. ## Out of scope - **Migrating the legacy `0001–0057` ADRs** to any new scheme — they keep their numbers; reserve-first applies going forward (next is `0060`). - **Automating PR creation/merge** — humans push and merge (D6). - **A CONTRIBUTING-side description of the reserve lock** — it's maintainer infrastructure, not contributor guidance (kept here instead).