diff --git a/docs/explore-notes/review-merge.md b/docs/explore-notes/review-merge.md index e8a47bd0..482c002a 100644 --- a/docs/explore-notes/review-merge.md +++ b/docs/explore-notes/review-merge.md @@ -1,8 +1,8 @@ # Review, merge & conflict resolution > **Explore-note — verify before trusting.** Distilled map of a subsystem, not authoritative. -> Last verified against `44d94caa` (2026-10-01), which made the parent's worktree the unit -> branch (children fast-forwarded into it). +> Last verified against `0e6a48e2` (2026-10-01): parent's worktree is the unit branch (children +> fast-forwarded into it); in a company repo approve ends in a pushed branch + draft PR. > Planning mode renders per file and `DiffLinesView` is retired. > Drift check: `git log --oneline 20bce9b..HEAD -- src/ClaudeDo.Worker/Lifecycle src/ClaudeDo.Worker/State src/ClaudeDo.Worker/Planning src/ClaudeDo.Ui/ViewModels/Conflicts src/ClaudeDo.Worker/External` > Stable structure only (no line numbers). See docs/explore-notes/README.md. @@ -28,6 +28,19 @@ There is no separate "Merge all" entry. - **Child of a unit** (parent has an `Active` worktree) → `ApproveAndMergeAsync` absorbs it into the unit branch (`UnitBranchService.AbsorbChildAsync`, ff-only) instead of merging to the target. - **No active worktree** (sandbox run) → straight to `Done`. +- **Company repo** (`PullRequestPublisher.IsCompanyRepoAsync`, origin matches + `company_remote_pattern`, checked live) with an `Active` worktree → `ApproveCompanyAsync` + instead of a merge, after the planning-parent gate and the child absorb: missing PAT / non-ADO + remote is `blocked` before anything runs, then the verify command in the **worktree** + (`skipVerify` ignored — no local merge to defer it to; red = `verify_failed`, nothing pushed), + then `PullRequestPublisher.PublishAsync`: `git push -u origin ` (PAT via `GIT_CONFIG_*` + env, `AzureDevOpsClient.GitAuthEnv`), find-or-create the draft PR, store + `worktrees.pull_request_url`, `WorktreeUpdated`. Result `pr_created` / `pr_updated` + URL + (`MergeResult.PullRequestUrl`; MCP puts it in `mergeMessage`). Task stays `WaitingForReview`, + worktree `Active`, ticket `InBearbeitung`; `Done` is the user's manual toggle after the ADO + merge. A PR failure after a successful push is `blocked` with `gepusht, PR fehlgeschlagen: …`. + ⚠️ Only approve branches — `merge_task`/the merge modal and the legacy-unit orchestrator still + merge locally in a company repo. Review transitions all live in `TaskStateService`: `SubmitForReviewAsync`, `SubmitForChildrenAsync`, `ApproveReviewAsync`, `RejectToQueueAsync`, `RejectToIdleAsync`, diff --git a/docs/open.md b/docs/open.md index 2fad2d0c..2d997e27 100644 --- a/docs/open.md +++ b/docs/open.md @@ -74,6 +74,24 @@ Bleibt manuell zu prüfen (UI-Pfad, nie sichtbar gelaufen): - [ ] Base-URL leeren → Menüeintrag verschwindet, keine Netzwerkaufrufe mehr. - [ ] Sichtprüfung des Settings-Tabs + des Ticket-Projekt-Dropdowns (nie gerendert gesehen). +## Offene Verifikation (2026-10-01, Unit-Branch Teil 3: Firmen-Modus / Draft-PR) + +Build + gefilterte Tests grün (Worker: AzureDevOps/CompanyFetchBase/CompanyApprove/CommitMessageBuilder, +Ui: TicketSettingsTab/MergeSectionCompanyRepo, Data: PromptFiles, Localization). **Nie gegen echtes ADO, +nicht visuell geprüft:** + +- [ ] Sichtprüfung: Settings-Reiter heißt „Externe Dienste", Abschnitt Azure DevOps (PAT, Test, Löschen); + Detail-Pane im Firmen-Repo: kein Ziel-Dropdown, Button „Draft-PR erstellen" → nach dem Approve + „PR aktualisieren" + Button „Pull Request öffnen"; Approve bleibt hinter „Diff öffnen" gegated. +- [ ] PAT eintragen, „Verbindung testen" — meldet Repo + Default-Branch der ersten Firmen-Liste. +- [ ] Ticket-Task in einem echten Firmen-Repo bis zum Draft-PR: Branch `/-` startet + von frisch gefetchtem `origin/`, Commit ohne `ClaudeDo-Task:`-Trailer, PR-Titel + `…, Resolves #`, Beschreibung mit Ticket-Link (Projekt-Id-Lookup über `GET /api/ticket/{id}` + ist nur gegen den Stub getestet) und `Geprüft:`-Zeile, Task bleibt WaitingForReview. +- [ ] Zweiter Approve nach Reject mit Feedback → `pr_updated`, derselbe PR bekommt die neuen Commits. +- [ ] Planning-Unit mit 2 Children im Firmen-Repo → ein PR für die ganze Unit. +- [ ] API-Version-Fallback (Server meldet „supports is X") ist nur gegen den Stub getestet. + ## Offene Verifikation (2026-10-01, Ticket-Import über alle Projekte) Build + Unit-Tests grün, **nicht visuell/live verifiziert**: diff --git a/src/ClaudeDo.Worker/CLAUDE.md b/src/ClaudeDo.Worker/CLAUDE.md index 09c8cadf..5263ce29 100644 --- a/src/ClaudeDo.Worker/CLAUDE.md +++ b/src/ClaudeDo.Worker/CLAUDE.md @@ -36,6 +36,8 @@ Worker/ Prime/ — "Prime Claude" schedules: PrimeScheduler, PrimeRunner, PrimePrompts, DailyPrepPrompt, PrimeScheduleValidation, NextDueCalculator, PrimeScheduleSignal Online/ — optional Online Inbox sync (off by default; zero network when disabled) + AzureDevOps/ — company-repo push + draft PR: AzureDevOpsClient (REST, port of pr-create.ps1), + PullRequestPublisher (push, find-or-create draft PR, pull_request_url) Tickets/ — Bandel ticket-system integration (per-list import + status write-back), inactive without ticket_api_base_url: TicketSystemConfig/TicketClientFactory, BandelTicketClient, TicketDtos, TicketStatusMap, TicketStatusSync, @@ -151,8 +153,8 @@ Full flow, invariants, and model/effort/max-turns resolution (including the low- - **ClaudeProcess** — spawns `claude -p --output-format stream-json --verbose --permission-mode auto` (or whatever app settings specify). Prompt via stdin, NDJSON from stdout. CancellationToken kills the process tree. Resolves `_cfg.ClaudeBin` via `ExecutableResolver` (`ClaudeDo.Data`, shared with `ClaudeCliPreflight` and the Installer's checks) before spawning — a `.cmd`/`.bat` shim (e.g. an npm-installed `claude`) is launched through `cmd.exe /c` since `UseShellExecute = false` can't exec a shim directly; a resolved `.exe` starts exactly as before. Throws if nothing resolves. *(Not yet on `main` — see `Environment Checks` in `ClaudeDo.Installer/CLAUDE.md`.)* - **ClaudeArgsBuilder** — `--model`, `--effort`, `--max-turns`, `--append-system-prompt`, `--agents`, `--json-schema`, `--resume` - **StreamAnalyzer** — parses NDJSON; extracts session_id, token counts, turn counts, result text, structured output. Replaced MessageParser. -- **WorktreeManager** — worktrees on `/-` branches, same leaf as the folder (`NameFor`, slug capped at 40 chars, umlauts transliterated). Typ from `BranchTypeFor` (`feat`→`feature`, `fix/refactor/docs/test/chore` as-is, anything else `chore`). Nummer = ticket id (own `TicketRef` or the parent's), else the task number — in a company repo (`CompanyRepo`, origin matches `company_remote_pattern`) without a ticket no number at all. A taken folder/branch name gets an 8-char task-id suffix, so the self-heal never force-deletes a foreign branch. Base = `rev-parse ` for a child whose parent has an Active worktree, else `list_config.base_branch`, else the checked-out HEAD. Commits with semantic messages, updates DB with head commit + diff stats -- **CommitMessageBuilder** — `{commitType}(slug): title\n\ndescription\n\nClaudeDo-Task: taskId`; `BuildMerge` is the merge-commit variant (`{commitType}(slug): merge title` + trailer). **Every merge caller passes a blank commit message on purpose** — `TaskMergeService` fills in `BuildMerge` from the task's commit type and its list's name, which is the only place that knows both. Don't reintroduce a caller-side literal. +- **WorktreeManager** — worktrees on `/-` branches, same leaf as the folder (`NameFor`, slug capped at 40 chars, umlauts transliterated). Typ from `BranchTypeFor` (`feat`→`feature`, `fix/refactor/docs/test/chore` as-is, anything else `chore`). Nummer = ticket id (own `TicketRef` or the parent's), else the task number — in a company repo (`CompanyRepo`, origin matches `company_remote_pattern`) without a ticket no number at all. A taken folder/branch name gets an 8-char task-id suffix, so the self-heal never force-deletes a foreign branch. Base = `rev-parse ` for a child whose parent has an Active worktree, else `list_config.base_branch`, else the checked-out HEAD — in a company repo a unit root first fetches its basis (see Azure DevOps below). Commits with semantic messages, updates DB with head commit + diff stats +- **CommitMessageBuilder** — `{commitType}(slug): title\n\ndescription\n\nClaudeDo-Task: taskId` (no trailer in company repos — the convention forbids signature trailers and nothing reads it); `BuildMerge` is the merge-commit variant (`{commitType}(slug): merge title` + trailer). **Every merge caller passes a blank commit message on purpose** — `TaskMergeService` fills in `BuildMerge` from the task's commit type and its list's name, which is the only place that knows both. Don't reintroduce a caller-side literal. - **TaskResetService** — discards a failed task's worktree, resets the row to Idle, preserves run history - **AgentFileService** — manages `~/.claudeDo/agents/*.md`; list/refresh via SignalR - **LogWriter** — async StreamWriter wrapper, auto-creates parent dirs @@ -273,10 +275,10 @@ non-obvious, behaviour-changing. A fixed bug is git history, not a finding. - `db_path`, `sandbox_root`, `log_root` - `worktree_root_strategy` (`sibling` | `central`), `central_worktree_root` - `queue_backstop_interval_ms` (30000) — also the gate/throttle recovery timer -- `company_remote_pattern` (`/_git/`) — Teilstring der origin-URL, an dem `CompanyRepo` ein Firmen-Repo erkennt; nur per Hand editierbar. +- `company_remote_pattern` (`/_git/`) — Teilstring der origin-URL, an dem `CompanyRepo` ein Firmen-Repo erkennt; nur per Hand editierbar. Schaltet Fetch vor dem Unit-Root, Approve = Push + Draft-PR und Commits ohne Trailer (siehe Azure DevOps). - `signalr_port` (47821), `claude_bin`, `ticket_api_base_url` — the two UI-writable fields (`claude_bin`: Settings → Dateien → CLAUDE CLI, hub `GetClaudeBin`/`SetClaudeBin`; - `ticket_api_base_url`: Settings → Ticketsystem, hub `GetTicketSettings`/`SetTicketApiBaseUrl`). + `ticket_api_base_url`: Settings → Externe Dienste, hub `GetTicketSettings`/`SetTicketApiBaseUrl`). Both edit the DI-singleton `WorkerConfig` in place *and* persist via their own `SaveKey` call (`SaveClaudeBin`/`SaveTicketApiBaseUrl`), so each applies without a restart. Every other field here is hand-edit only. @@ -341,6 +343,39 @@ distinguish the two singletons). The token never touches `worker.config.json` or `TicketStatusMap.NameOf` supplies the canonical name. ⚠️ The base URL must be the **IP** — `bandelapis.fb-tuning.local` still refuses the connection. +## Azure DevOps (company repos) + +Active when origin matches `company_remote_pattern` (live `git remote get-url`, no cache). The PAT +(scope Code Read & Write) is DPAPI-encrypted at `~/.claudeDo/azdo.pat` (DI marker +`AzureDevOpsPatStore`), set via Settings → Externe Dienste (hub `GetAzureDevOpsSettings` / +`SetAzureDevOpsToken` / `ClearAzureDevOpsToken` / `TestAzureDevOpsConnection` — the test reads the +first list whose origin matches and parses). Deliberately independent of the work-conventions +plugin's PAT file. + +- **`AzureDevOpsClient`** — the only HTTP surface. `ParseRemote` splits at `/_git/` (API base = + everything before it, embedded credentials stripped, http(s) only). `GitAuthEnv(pat)` is the + env for every git network call: `GIT_TERMINAL_PROMPT=0`, `GCM_INTERACTIVE=never`, PAT as + `http.extraHeader` via `GIT_CONFIG_COUNT/KEY_0/VALUE_0` — never on a command line. API version + starts at 6.0 and retries once with the version an out-of-range error names. ⚠️ A rejected PAT + is HTTP **203** + an HTML login page, which `IsSuccessStatusCode` counts as success — handled + explicitly. +- **Fetch before a unit root** (`WorktreeManager.FetchCompanyBaseAsync`, once per unit root, never + per child): basis = `base_branch` → ADO `defaultBranch` → `origin/HEAD`; `git fetch origin + +refs/heads/:refs/remotes/origin/`; the worktree starts from `origin/`. Local `` is + pulled along best-effort: not checked out → `git fetch . origin/:` (no network, git + refuses a non-ff), checked out + clean → `merge --ff-only`, dirty/diverged → warning only. A + failed fetch warns and continues from the local branch — it never fails the task. +- **Approve** → `TaskMergeService.ApproveCompanyAsync` → `PullRequestPublisher`; details in + [review-merge](../../docs/explore-notes/review-merge.md). PR title + `(): , Resolves #<ticket>`; description + `Resolves [#id](https://ticket.fb-tuning.local/board/project/<p>/ticket/<id>)` (project id = walk + `GET /api/ticket/{id}` up until `parentTicketID` is null — a ticket's parent is its project, a + task's parent is a ticket, live-checked 2026-10-01; unknown → plain `Resolves #id`), the task + description, `Geprüft: …` with only what actually ran. Target = `base_branch`, else the ADO + default branch. +- Not done on purpose: PR status polling / auto-`Done`, other PR providers, worktree cleanup on + `Done`, fetch in private repos, a UI for `company_remote_pattern`. + ## Notes - The worker runs standalone — start it separately from the UI. Loopback only (127.0.0.1).