docs(spec): per-list base branch and fetch before unit branch

This commit is contained in:
mika kuns
2026-10-01 10:16:32 +02:00
parent 0e9373c083
commit 3dc735b2af
@@ -1,6 +1,6 @@
# Unit-Branch + Draft-PR — Design
**Datum:** 2026-10-01 · **Status:** Entwurf, Review durch Mika ausstehend
**Datum:** 2026-10-01 · **Status:** abgenommen (mit Basis-Branch pro Liste + Fetch)
**Diagramme:** Obsidian `07 - Plattform & Technik/ClaudeDo/ClaudeDo - Branch und PR Ablauf.md`
**Ersetzt:** die offene Entscheidung aus `2026-08-06-planning-chain-fork-base-design.md`
@@ -33,6 +33,8 @@ Vorgängers.
| PR-Automatik | Kein Auto-PR bei einem Statuswechsel. Automatisiert wird über den Merge-Helper, dessen `approve` dann den PR erzeugt. |
| ADO-Zugang | Eigener PAT (Scope Code Read & Write). Er wird per `DpapiTokenStore` in `~/.claudeDo/azdo.pat` gespeichert, ohne Abhängigkeit auf das work-conventions-Plugin. |
| dependsOn über Units | Wartet bewusst auf den echten Merge. Abhängige Arbeit gehört in eine Unit mit einem PR. |
| Basis-Branch | Pro Liste einstellbar (`list_config.base_branch`, z.B. `dev` für die LagerApp). Leer heißt: privat der gerade ausgecheckte Branch des Main-Checkouts (wie heute), Firma der Default-Branch des ADO-Repos. Gilt als Basis des Unit-Branches, als PR-Ziel und als Vorauswahl des privaten Merge-Ziels. |
| Basis aktualisieren | Firmen-Repo: `git fetch origin <basis>`, bevor der Unit-Branch angelegt wird. Der Unit-Branch startet von `origin/<basis>`, der lokale Basis-Branch wird best-effort per fast-forward nachgezogen. |
## 1. Branch-Naming (`WorktreeManager`)
@@ -69,6 +71,37 @@ Vorgängers.
**Basis eines Childs** (`ResolveBaseCommitAsync`): Das ist immer der aktuelle Head des
Unit-Branches (`rev-parse <unitBranch>`). Der Sonderfall „nur bei `PlanningPhase.None`“ fällt weg.
**Basis eines Unit-Roots** (Einzeltask, Parent):
- **Privat:** `list_config.base_branch`, sonst der ausgecheckte Branch des Main-Checkouts. Heute
ist es `RevParseHeadAsync`. Ist `base_branch` gesetzt, wird es `rev-parse <base_branch>`, auch
wenn gerade ein anderer Branch ausgecheckt ist.
- **Firma:**
- Zuerst `git fetch origin <basis>`. `<basis>` ist `base_branch`, sonst der Default-Branch des
Repos per ADO-API, ersatzweise `git symbolic-ref refs/remotes/origin/HEAD`.
- Der Unit-Branch startet von `origin/<basis>`.
- Dann wird der lokale `<basis>` best-effort nachgezogen:
- nicht ausgecheckt: `git fetch origin <basis>:<basis>` (lehnt einen nicht-fast-forward
selbst ab)
- ausgecheckt und sauber: `git -C <workingDir> merge --ff-only origin/<basis>`
- schmutzig oder divergiert: Warnung ins Log, kein Eingriff in den Checkout
- Schlägt der Fetch fehl (offline, keine Credentials), wird gewarnt und es geht mit dem
lokalen `<basis>` weiter. Der Task scheitert daran nicht.
- Der Fetch nutzt dieselbe PAT-Übergabe wie der Push (Abschnitt 3).
- Der Fetch läuft einmal pro Unit-Root, nicht pro Child. Children starten vom Unit-Branch.
**Spalte `list_config.base_branch`** (nullable string, Migration):
- Sie wird im Listeneinstellungs-Modal gepflegt, als Freitext mit Vorschlägen aus
`ListLocalBranchesAsync`, und in `get_list_config` mitgeliefert.
- ⚠️ `ListRepository.SetConfigAsync` kopiert verbatim (siehe `TicketProjectId`). Jeder Schreiber
muss den Wert durchreichen, sonst setzt ihn z.B. `set_list_config` still zurück:
`WorkerHub.UpdateListConfig`, `ConfigMcpTools`, alle Stellen, die eine frische
`ListConfigEntity` bauen.
- `UpdateListConfigDto` braucht ihn tri-state wie `TicketProjectId`, damit ein Aufrufer, der das
Feld nicht kennt, es nicht löscht.
- **Merge-Ziel privat:** `TaskMergeService.GetTargetsAsync` setzt `DefaultBranch` auf
`base_branch`, falls gesetzt. Das leere Ziel (`ApproveAndMergeAsync:1077`, `PreviewAsync:910`)
löst ebenfalls auf `base_branch` auf, sonst wie heute.
**Absorbieren eines Childs:** Neu ist `UnitBranchService.AbsorbChildAsync(childId)`, das in
`git -C <parentWorktree> merge --ff-only <childBranch>` mündet.
- Die Kette ist seriell (`BlockedByTaskId`), und das Child startet vom Head des Unit-Branches.
@@ -119,8 +152,8 @@ einem Folge-Task, sobald keine solchen Units mehr offen sind.
- `GET …/pullrequests?searchCriteria.sourceRefName=refs/heads/<branch>&searchCriteria.status=active`.
Gibt es schon einen PR, wird er wiederverwendet (idempotent, auch wenn der Skill in der
Session schon einen angelegt hat).
- Sonst `POST …/pullrequests` mit `isDraft: true` und
`targetRefName = <defaultBranch des Repos>` (`GET …/repositories/<repo>`).
- Sonst `POST …/pullrequests` mit `isDraft: true` und `targetRefName = <basis>`. Das ist
`list_config.base_branch`, sonst `defaultBranch` aus `GET …/repositories/<repo>`.
- Die API-Version fällt auf die zurück, die der Server in seiner Fehlermeldung nennt, wie im
Skript.
4. Die **PR-URL** wird gespeichert, in der neuen nullable Spalte `worktrees.pull_request_url`
@@ -181,8 +214,7 @@ Die Prompts in `PromptFiles` (merge und merge_final) bekommen einen Firmen-Repo-
PR-API ist ADO-spezifisch.
- Aufräumen von Worktree und lokalem Branch, wenn ein Firmen-Task auf `Done` gesetzt wird. Das
geht über die bestehende Worktree-Übersicht.
- `git fetch` vor dem Anlegen des Unit-Branches. Die Basis ist der lokale `main`-HEAD wie heute,
siehe offene Frage.
- `git fetch` in privaten Repos. Die Basis bleibt dort lokal.
- Eine UI für `company_remote_pattern`.
## 7. Tests
@@ -204,12 +236,14 @@ Keine Tests mit der echten Claude-CLI und keine gegen das echte ADO.
- `AzureDevOpsClient` wird gefakt: PR anlegen, vorhandenen PR wiederverwenden, Verify rot
heißt kein Push, PR-Fehler nach dem Push.
- **Remote-Parsing:** die Fälle aus dem `-SelfTest` des Skripts.
- **Basis:**
- `base_branch` gesetzt: Der Unit-Root startet davon, auch wenn ein anderer Branch ausgecheckt
ist.
- Firma: Nach dem Fetch startet der Unit-Root von `origin/<basis>`. Das testet ein Bare-Repo,
das einen Commit voraus ist.
- Der lokale Basis-Branch wird nachgezogen, ein schmutziger Checkout bleibt unangetastet.
- Ein Fetch-Fehler bricht den Task nicht.
- `SetConfigAsync`, `set_list_config` und `UpdateListConfig` erhalten `base_branch`.
- **Manueller Smoke-Test (Mika):** PAT eintragen, Test-Button. Ein Ticket-Task in einem echten
Firmen-Repo bis zum Draft-PR durchspielen, danach eine Planning-Unit mit 2 Children.
## Offene Frage
- **Basis im Firmen-Repo:** Der lokale `main` ist in Firmen-Repos oft veraltet. Vorschlag für
später: `git fetch origin <default>` und dann `origin/<default>` als Basis des Unit-Branches.
In v1 ist das draußen. Wenn es dich jetzt schon stört, ist es ein kleiner Zusatz in
`ResolveBaseCommitAsync`.