16 KiB
Unit-Branch + Draft-PR — Design
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
Ziel
Aufgaben starten und später zu fertigen PRs zurückkommen. In Firmen-Repos (Azure DevOps Server)
heißen Branches nach dem work-conventions-Schema, und das Review endet in einem Draft-PR statt
in einem lokalen Merge auf main. Nebenbei sehen Planning-Children endlich die Arbeit ihres
Vorgängers.
Begriffe
- Unit: ein einzelner Task, oder ein Parent (Planning oder Improvement) samt Children.
- Unit-Branch: der eine Branch, auf dem die ganze Arbeit einer Unit landet. Er gehört dem Worktree des Unit-Roots (Einzeltask bzw. Parent).
- Firmen-Repo: Die
origin-URL enthältcompany_remote_pattern(worker.config.json, Default/_git/, nur per Hand editierbar). Geprüft wird live übergit remote get-url originund ohne Cache.
Entscheidungen
| Frage | Entscheidung |
|---|---|
| Branchname | Einheitlich <typ>/<nummer>-<slug>. Die Nummer ist die Ticket-Id aus TicketRef (beim Child die des Unit-Roots), sonst privat die Task-Nummer. Im Firmen-Repo ohne Ticket entfällt sie (<typ>/<slug>). claudedo/{nr}-… fällt weg. Planning-Session-Worktrees bleiben claudedo/planning/{guid}. |
| Typ | feat→feature. fix, refactor, docs, test, chore bleiben, wie sie sind. Alles andere (ci, perf, style, build) wird chore. |
| Slug | CommitMessageBuilder.ToSlug plus Umlaut-Auflösung (ä→ae, ö→oe, ü→ue, ß→ss), ≤40 Zeichen wie heute. |
| Children | Starten vom aktuellen Head des Unit-Branches. Ist ein Child fertig, wird der Unit-Branch sofort per fast-forward auf den Head des Childs gezogen. Das gilt privat und in Firmen-Repos. |
| Review-Ende privat | Unverändert: Approve merged den Unit-Branch auf main, dann läuft Verify auf main. |
| Review-Ende Firma | Approve führt Verify im Worktree des Unit-Roots aus, dann Push und Draft-PR. Der Task bleibt WaitingForReview mit PR-Link. Done setzt Mika per Toggle nach dem ADO-Merge. |
| 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)
NameFor(task, ticketId, isCompany)liefert den Leaf<nummer>-<slug>bzw.<slug>, undBranchForliefert<typ>/<leaf>. Der Ordnername des Worktrees bleibt der Leaf.- Kollisionsprüfung (heute
Contains($"claudedo/{name}"), Zeile 83) prüft den neuen vollen Branchnamen. Bei einem Treffer kommt das Id-Suffix dran, wie heute. - Self-Heal (
git branch -Dnach „already exists“) läuft nur noch, wenn der Branch in einerworktrees-Zeile steht, deren Task der aktuelle Task ist. Sonst gibt es keinen Delete, sondern das Id-Suffix, und der Branch des Nutzers bleibt unangetastet. Ohne das Präfixclaudedo/ist das Pflicht, sonst löscht ClaudeDo einen fremdenfeature/…-Branch. UntrackedWorktreeRecoveryfiltert statt nach dem Präfix nach „Worktree-Pfad liegt unter einer ClaudeDo-Worktree-Root (.claudedo-worktreesbzw. Central-Root), aber nicht unterplanning/“.PlanningAggregator.CleanupIntegrationBranchAsync:137(Präfix-Check beim Checkout-Fallback) wird mit Abschnitt 3 ohnehin hinfällig.- Bestehende Worktrees behalten ihren Namen. Der Branchname steht in der DB und wird nie neu abgeleitet, eine Migration ist also nicht nötig.
- Bugfix nebenbei:
DetailsIslandViewModel.cs:1223/1385baut für Dialogtexteclaudedo/{Task.Id}, das ist schon heute falsch. Künftig steht dortworktree.BranchName. Das literale"← main"(:670/:933) zeigt das echte Ziel an.
2. Unit-Branch und Children
Wer besitzt den Unit-Branch?
- Einzeltask: sein eigener Worktree. Am Ablauf ändert sich nichts.
- Improvement-Parent: sein eigener Worktree, den er schon heute hat. Seine Children starten
schon heute von
parent.HeadCommit. - Planning-Parent: hat heute keinen Worktree. Neu:
QueuePlanAsynclegt perWorktreeManager.CreateAsynceinen Worktree für den Parent an, auf dem Unit-Branch und mitmainHEAD als Basis. Damit hat jeder Parent eineworktrees-Zeile, die den Unit-Branch und später die PR-URL trägt.
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 esRevParseHeadAsync. Istbase_branchgesetzt, wird esrev-parse <base_branch>, auch wenn gerade ein anderer Branch ausgecheckt ist. - Firma:
- Zuerst
git fetch origin <basis>.<basis>istbase_branch, sonst der Default-Branch des Repos per ADO-API, ersatzweisegit 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
- nicht ausgecheckt:
- 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).
- Zuerst
- 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 inget_list_configmitgeliefert. - ⚠️
ListRepository.SetConfigAsynckopiert verbatim (sieheTicketProjectId). Jeder Schreiber muss den Wert durchreichen, sonst setzt ihn z.B.set_list_configstill zurück:WorkerHub.UpdateListConfig,ConfigMcpTools, alle Stellen, die eine frischeListConfigEntitybauen. UpdateListConfigDtobraucht ihn tri-state wieTicketProjectId, damit ein Aufrufer, der das Feld nicht kennt, es nicht löscht.- Merge-Ziel privat:
TaskMergeService.GetTargetsAsyncsetztDefaultBranchaufbase_branch, falls gesetzt. Das leere Ziel (ApproveAndMergeAsync:1077,PreviewAsync:910) löst ebenfalls aufbase_branchauf, 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. Deshalb ist es immer ein fast-forward, Konflikte sind ausgeschlossen. Lässt es sich nicht fast-forwarden, z.B. weil jemand ein Child manuell parallel per „Run now“ gestartet hat, wird das ChildFailedmit einer klaren Meldung, ohne Merge-Versuch.ponytail: kein Merge-Fallback; einbauen, falls parallele Children ein echter Fall werden. - Danach wird der Worktree des Childs entfernt, der Branch gelöscht und die Zeile auf
Mergedgesetzt (MergeCommit= null, denn es gibt keinen Merge-Commit).revert_mergegreift auf Child-Ebene deshalb nicht, revertiert wird die ganze Unit. - Aufgerufen wird es aus
TaskRunner.HandleSuccessdirekt vor_state.CompleteAsync. Ein ff-Fehler wird so zuFailAsyncstatt zu einem Unblock des Nachfolgers. - Interaktive Children landen in
WaitingForReview, und ihr Approve geht durchTaskMergeService.ApproveAndMergeAsync. Dort gilt neu: Hat der Task einen Parent mit eigenem Worktree, wirdAbsorbChildAsyncaufgerufen statt auf das Ziel gemergt. - Kein Git-Code in
TaskStateServiceoderPlanningChainCoordinator, die bleiben git-frei.
Was dabei wegfällt: Den Unit-Merge Child für Child am Ende gibt es für neue Units nicht mehr.
Ein Parent wird beim Approve wie ein Einzeltask behandelt, über seinen eigenen Worktree in
TaskMergeService.ApproveAndMergeAsync. Auch der Integration-Branch
(PlanningAggregator, planning/{slug}-integration, dafür checkout -b im Main-Checkout) ist
für neue Units unnötig, denn der kombinierte Diff ist unitBase..unitHead.
Routing beim Approve (WorkerHub.ApproveReview, ExternalMcpService.ReviewTask): Heute
geht jeder Task mit Children an den Orchestrator. Neu gilt: Gibt es ein Done-Child mit einem
Active-Worktree, also eins, das nicht absorbiert wurde, läuft der Legacy-Orchestrator. Sonst
geht es über ApproveAndMergeAsync auf den Worktree des Parents.
Altbestand: Parents ohne eigenen Worktree (vor dem Rollout geplant) laufen weiter über
PlanningMergeOrchestrator und den bestehenden Aggregator. Diesen Legacy-Pfad entfernen wir in
einem Folge-Task, sobald keine solchen Units mehr offen sind.
3. Approve im Firmen-Repo
TaskMergeService.ApproveAndMergeAsync verzweigt nach IsCompanyRepo(list):
- Verify im Worktree des Unit-Roots (
IVerifyCommandRunner.RunAsync(worktreePath, …)), nicht im Main-Checkout. Ohne Verify-Command entfällt der Schritt. Rot heißt: Ergebnisverify_failed, der Task bleibtWaitingForReview, kein Push.skipVerifywird im Firmen-Modus ignoriert, weil kein Merge landet, auf den man das Verify verschieben könnte. - Push mit
git -C <worktree> push -u origin <unitBranch>. Der PAT geht alshttp.extraHeaderüberGIT_CONFIG_COUNT/GIT_CONFIG_KEY_0/GIT_CONFIG_VALUE_0in der Prozess-Umgebung mit. Er steht also nicht in der Kommandozeile, und es gibt keinen Credential-Manager-Prompt im Hintergrundprozess. - PR, neuer
AzureDevOpsClient, portiert auspr-create.ps1:- Der Remote wird an
/_git/zerlegt: API-Basis = alles davor, Repo = Segment danach. Eingebettete Zugangsdaten fliegen raus. 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 …/pullrequestsmitisDraft: trueundtargetRefName = <basis>. Das istlist_config.base_branch, sonstdefaultBranchausGET …/repositories/<repo>. - Die API-Version fällt auf die zurück, die der Server in seiner Fehlermeldung nennt, wie im Skript.
- Der Remote wird an
- Die PR-URL wird gespeichert, in der neuen nullable Spalte
worktrees.pull_request_url(Migration). Danach folgenWorktreeUpdatedund der Rückgabestatuspr_createdbzw.pr_updated. - Der Task bleibt
WaitingForReview, der Worktree bleibtActive, und das Ticket bleibt aufInBearbeitung.
PR-Inhalt:
- Titel:
<commitType>(<list-slug>): <titel>, Resolves #<ticket>. Ohne Ticket entfälltResolves. - Beschreibung:
Resolves [#412](<ticket-url>). Die Projekt-Id kommt überBandelTicketClient.GetTicketAsync→ParentTicketID(bei Tasks eine Ebene höher). Ohne Id gibt es keinen Link.- Leerzeile, dann die Task-Beschreibung.
- Leerzeile, dann
Geprüft: <verify command> grünbzw.Geprüft: nichts automatisch.
Commits im Firmen-Repo: CommitMessageBuilder lässt den Trailer ClaudeDo-Task: weg. Er
wird nirgends gelesen, und die Konvention verbietet Signatur-Trailer.
Rerun nach Änderungswunsch: Ein normaler Reject mit Feedback, der auf demselben Worktree weiterläuft. Der nächste Approve pusht nach, und der vorhandene PR aktualisiert sich.
Fehler: Fehlt der PAT, wird der Push abgelehnt oder scheitert ein REST-Aufruf, dann landen
Statusmeldung und Footer-Strip (FlashFooterError) beim Nutzer, und der Task bleibt
WaitingForReview. Scheitert der PR nach einem erfolgreichen Push, sagt die Meldung das genau
so: „gepusht, PR fehlgeschlagen: …“.
Laufzeit: Verify plus Push können länger als 5 s dauern. Der MCP-Pfad meldet deshalb Progress
über ProgressReporter, sonst bricht der MCP-Client nach 300 s Leerlauf ab.
4. Merge-Helper
Die Prompts in PromptFiles (merge und merge_final) bekommen einen Firmen-Repo-Absatz:
review_task approveliefertpr_created/pr_updatedmit URL.verify_mergesentfällt, weil das Verify bereits im Approve läuft.- Die Abschluss-Zusammenfassung listet die PR-Links auf.
- Die Regel „erst mergen, dann neu starten“ bleibt harmlos, da
mainsich nicht bewegt.
5. UI
- Der Settings-Reiter „Ticketsystem“ heißt künftig „Externe Dienste“. Er bekommt einen zweiten Abschnitt Azure DevOps mit PAT-Feld, Test-Button und Löschen-Button, gespiegelt vom Ticket-PAT.
- Neue Hub-Methoden:
GetAzureDevOpsSettings,SetAzureDevOpsToken,ClearAzureDevOpsToken,TestAzureDevOpsConnection. Neuer DI-MarkerAzureDevOpsPatStore. - Der Test braucht ein Firmen-Repo als Kontext: Er nimmt die erste Liste, deren
originpasst, und ruftGET …/repositories/<repo>auf. - Im Detail-Pane heißt der Approve-Button im Firmen-Repo „Draft-PR erstellen“. Er ist weiter hinter „Diff öffnen“ gegated, und das Ziel-Dropdown ist ausgeblendet. Ist eine PR-URL gesetzt, erscheint ein Link und der Button heißt „PR aktualisieren“.
- Locale-Keys kommen in
en.jsonundde.jsongleichzeitig hinzu.
6. Bewusst draußen
- PR-Status-Polling bzw. automatisches
Donenach dem ADO-Merge. - Andere PR-Provider als Azure DevOps.
company_remote_patternschaltet nur das Schema um, die PR-API ist ADO-spezifisch. - Aufräumen von Worktree und lokalem Branch, wenn ein Firmen-Task auf
Donegesetzt wird. Das geht über die bestehende Worktree-Übersicht. git fetchin privaten Repos. Die Basis bleibt dort lokal.- Eine UI für
company_remote_pattern.
7. Tests
Keine Tests mit der echten Claude-CLI und keine gegen das echte ADO.
- Naming: Typ-Mapping, Nummernregel (Ticket, privat, Firma ohne Ticket), Umlaute,
Kollision mit einem fremden Branch (der Branch überlebt, das Suffix greift), Self-Heal löscht
nur DB-eigene Branches. Die betroffenen Asserts in
WorktreeManagerTests(:69,:131/138,:160,:314) passe ich an. - Unit-Branch (echtes git, Worker.Tests):
- Ein Child startet vom Head des Unit-Branches.
- Zwei serielle Children werden per fast-forward absorbiert, und Child 2 sieht die Dateien von Child 1.
- Ein nicht fast-forwardbares Child wird
Failed. - Ein Planning-Parent bekommt seinen Worktree bei
QueuePlanAsync.
- Approve Firma:
- Ein lokales Bare-Repo dient als
origin, und das Muster wird auf dessen Pfad gesetzt. AzureDevOpsClientwird gefakt: PR anlegen, vorhandenen PR wiederverwenden, Verify rot heißt kein Push, PR-Fehler nach dem Push.
- Ein lokales Bare-Repo dient als
- Remote-Parsing: die Fälle aus dem
-SelfTestdes Skripts. - Basis:
base_branchgesetzt: 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_configundUpdateListConfigerhaltenbase_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.