From ac131f7f539e57bcdf6e440272677c63cc51cb72 Mon Sep 17 00:00:00 2001 From: DevOps Date: Sun, 21 Jun 2026 21:30:52 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Parent-Child=20TaskFlow=20=E2=80=94=20D?= =?UTF-8?q?elegated=20durch=20sichtbare=20Child-Tasks=20ersetzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delegated State aus Board, Entities, DTOs, Frontend-Spalten und Tests entfernt - Parent-Tasks bleiben InProgress waehrend delegierter Agentenarbeit - Child-Tasks laufen sichtbar mit normalen States und parentTaskId - Doku: README, Phase 3, Changelog, Controller-Kommentare angepasst - openclaw-task-board-flow.md als Referenzdoku hinzugefuegt - 73/73 Backend-Tests gruen, Frontend-Build gruen --- README.md | 11 +- backend-tests/TaskBoardTests.cs | 16 +- backend/Controllers/TasksController.cs | 2 +- backend/Data/Entities.cs | 12 +- backend/Data/Identity.cs | 1 + backend/Models/Dashboard.cs | 1 - backend/Services/TaskService.cs | 29 ++- docs/openclaw-task-board-flow.md | 320 +++++++++++++++++++++++++ frontend/src/services/api.ts | 2 + frontend/src/stores/tasks.ts | 14 +- frontend/src/types/dashboard.ts | 4 +- frontend/src/views/TaskBoardView.vue | 135 +++++------ frontend/src/views/TaskDetailView.vue | 37 ++- phases/changelog.md | 3 +- phases/phase-3.md | 10 +- 15 files changed, 464 insertions(+), 133 deletions(-) create mode 100644 docs/openclaw-task-board-flow.md diff --git a/README.md b/README.md index b97dfd5..c5a54eb 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ Legacy ModuleView routes (not standalone, rendered through `ModuleView.vue`): | Route | Name | Description | |---|---|---| | `/projects` | Projects | Project portfolio | -| `/tasks` | Task Board | Task board | +| `/tasks` | Task Board | Task board with visible parent/child agent flow | | `/models` | Models | Provider routing status | | `/activity` | Activity | Audit timeline | | `/chat` | Mobile Chat | Owner-chat preview | @@ -189,6 +189,15 @@ Legacy ModuleView routes (not standalone, rendered through `ModuleView.vue`): |---|---|---| | `GET` | `/api/v1/operations/snapshot` | Full operations snapshot (runtime, agents, projects, tasks, activity, metrics) | +### Parent/Child task flow + +The Task Board now models OpenClaw delegation as a visible parent/child flow: +- Iris keeps the parent task `In progress` while delegated work is running. +- Delegated agent work is represented as visible child tasks linked via `parentTaskId`. +- Child tasks use the normal visible states (`Backlog`, `In progress`, `Review`, `Blocked`, `Done`) instead of a separate hidden delegation lane. +- Agent progress hints on parent tasks derive from recent activity and child-task status summaries. +- Full workflow documentation: [`docs/openclaw-task-board-flow.md`](docs/openclaw-task-board-flow.md) + ### Projects | Method | Path | Description | diff --git a/backend-tests/TaskBoardTests.cs b/backend-tests/TaskBoardTests.cs index 43eae58..6daa280 100644 --- a/backend-tests/TaskBoardTests.cs +++ b/backend-tests/TaskBoardTests.cs @@ -10,13 +10,11 @@ public class TaskBoardTests [Theory] [InlineData("Backlog", "offen")] [InlineData("In progress", "inProgress")] - [InlineData("Delegated", "delegated")] [InlineData("Review", "review")] [InlineData("Blocked", "blocked")] [InlineData("Done", "done")] [InlineData("backlog", "offen")] [InlineData("in progress", "inProgress")] - [InlineData("delegated", "delegated")] [InlineData("review", "review")] [InlineData("blocked", "blocked")] [InlineData("done", "done")] @@ -35,7 +33,6 @@ public class TaskBoardTests [InlineData("offen", "Backlog")] [InlineData("inProgress", "In progress")] [InlineData("inprogress", "In progress")] - [InlineData("delegated", "Delegated")] [InlineData("review", "Review")] [InlineData("blocked", "Blocked")] [InlineData("done", "Done")] @@ -49,16 +46,15 @@ public class TaskBoardTests Assert.Equal(expected, result); } - // ── TaskStateHelper: AllStates has 6 entries ── + // ── TaskStateHelper: AllStates has 5 entries ── [Fact] - public void AllStates_ContainsAllSixStates() + public void AllStates_ContainsAllFiveStates() { var states = TaskStateHelper.AllStates; - Assert.Equal(6, states.Length); + Assert.Equal(5, states.Length); Assert.Contains("Backlog", states); Assert.Contains("In progress", states); - Assert.Contains("Delegated", states); Assert.Contains("Review", states); Assert.Contains("Blocked", states); Assert.Contains("Done", states); @@ -69,7 +65,6 @@ public class TaskBoardTests [Theory] [InlineData("Backlog", true)] [InlineData("In progress", true)] - [InlineData("Delegated", true)] [InlineData("Review", true)] [InlineData("Blocked", true)] [InlineData("Done", true)] @@ -89,7 +84,6 @@ public class TaskBoardTests [InlineData("In progress", true)] [InlineData("Blocked", true)] [InlineData("Backlog", false)] - [InlineData("Delegated", false)] [InlineData("Review", false)] [InlineData("Done", false)] [InlineData(null, false)] @@ -104,7 +98,6 @@ public class TaskBoardTests [InlineData("Done", true)] [InlineData("Backlog", true)] [InlineData("In progress", false)] - [InlineData("Delegated", false)] [InlineData("Review", false)] [InlineData("Blocked", false)] [InlineData(null, false)] @@ -118,7 +111,6 @@ public class TaskBoardTests [Theory] [InlineData("Backlog", "Offen")] [InlineData("In progress", "In Bearbeitung")] - [InlineData("Delegated", "Delegiert")] [InlineData("Review", "Review")] [InlineData("Blocked", "Blockiert")] [InlineData("Done", "Erledigt")] @@ -136,7 +128,7 @@ public class TaskBoardTests [Fact] public void ToStateString_And_ToTaskState_RoundTrip() { - var states = new[] { TaskState.Backlog, TaskState.InProgress, TaskState.Delegated, TaskState.Review, TaskState.Blocked, TaskState.Done }; + var states = new[] { TaskState.Backlog, TaskState.InProgress, TaskState.Review, TaskState.Blocked, TaskState.Done }; foreach (var state in states) { var str = state.ToStateString(); diff --git a/backend/Controllers/TasksController.cs b/backend/Controllers/TasksController.cs index 4c3ec3d..d833b36 100644 --- a/backend/Controllers/TasksController.cs +++ b/backend/Controllers/TasksController.cs @@ -118,7 +118,7 @@ public class TasksController(ITaskService taskService) : ControllerBase => Results.Ok(await taskService.GetBoardAsync(ct)); /// - /// Setzt stale Tasks (InProgress/Delegated, älter als N Stunden) zurück auf Backlog. + /// Setzt stale Tasks (InProgress, älter als N Stunden) zurück auf Backlog. /// Wird vom Iris Autonomous Worker genutzt. /// [AllowAnonymous] diff --git a/backend/Data/Entities.cs b/backend/Data/Entities.cs index ecc4c06..a06cee4 100644 --- a/backend/Data/Entities.cs +++ b/backend/Data/Entities.cs @@ -18,7 +18,6 @@ public enum TaskState { Backlog, InProgress, - Delegated, Blocked, Done, Review @@ -30,7 +29,6 @@ public static class TaskStateHelper { [TaskState.Backlog] = "Backlog", [TaskState.InProgress] = "In progress", - [TaskState.Delegated] = "Delegated", [TaskState.Blocked] = "Blocked", [TaskState.Done] = "Done", [TaskState.Review] = "Review" @@ -40,7 +38,6 @@ public static class TaskStateHelper { ["Backlog"] = TaskState.Backlog, ["In progress"] = TaskState.InProgress, - ["Delegated"] = TaskState.Delegated, ["Blocked"] = TaskState.Blocked, ["Done"] = TaskState.Done, ["Review"] = TaskState.Review @@ -51,14 +48,13 @@ public static class TaskStateHelper { ["Backlog"] = "Offen", ["In progress"] = "In Bearbeitung", - ["Delegated"] = "Delegiert", ["Review"] = "Review", ["Blocked"] = "Blockiert", ["Done"] = "Erledigt" }; /// Valid task-state string values for API validation. - public static readonly string[] AllStates = ["Backlog", "In progress", "Delegated", "Blocked", "Done", "Review"]; + public static readonly string[] AllStates = ["Backlog", "In progress", "Blocked", "Done", "Review"]; /// Convert a TaskState enum to its API string representation. public static string ToStateString(this TaskState state) => StateToString[state]; @@ -87,7 +83,7 @@ public static class TaskStateHelper /// Returns true if the caller is allowed to change this task's state. /// POLICY: /// - **Iris und Bao** dürfen Status ändern / verschieben. - /// - Sub-agents (programmer, reviewer, architekt) dürfen NIEMALS Status ändern. + /// - Sub-agents (programmer, reviewer, architekt, researcher, executor) dürfen NIEMALS Status ändern. /// - 'nexus-system' ist ein technischer Fallback für automatische Cron/Reset-Workflows. /// - Jeder andere (unbekannt, leer) wird abgewiesen. /// @@ -96,7 +92,7 @@ public static class TaskStateHelper var caller = callerAgent?.Trim().ToLowerInvariant() ?? ""; // Sub-agents must never move state - var subAgents = new HashSet { "programmer", "reviewer", "architekt" }; + var subAgents = new HashSet { "programmer", "reviewer", "architekt", "researcher", "executor" }; if (subAgents.Contains(caller)) return false; // Technischer Fallback: nur für interne System-Operationen (Cron, ResetStale) @@ -129,7 +125,6 @@ public static class TaskStateHelper { "backlog" => "offen", "in progress" => "inProgress", - "delegated" => "delegated", "review" => "review", "blocked" => "blocked", "done" => "done", @@ -146,7 +141,6 @@ public static class TaskStateHelper { "offen" => "Backlog", "inprogress" => "In progress", - "delegated" => "Delegated", "review" => "Review", "blocked" => "Blocked", "done" => "Done", diff --git a/backend/Data/Identity.cs b/backend/Data/Identity.cs index dd39d2b..4b450d8 100644 --- a/backend/Data/Identity.cs +++ b/backend/Data/Identity.cs @@ -34,6 +34,7 @@ public class NexusUser /// if the underlying data is deleted. This is the single guard that /// prevents owner-password drift after DB resets or volume recreations. /// +[Table("SeedAudit")] public class SeedAudit { [Key] diff --git a/backend/Models/Dashboard.cs b/backend/Models/Dashboard.cs index 6da7d83..cd5ea9e 100644 --- a/backend/Models/Dashboard.cs +++ b/backend/Models/Dashboard.cs @@ -138,7 +138,6 @@ public sealed record AgentActivityEntry( public sealed record BoardResponse( List Offen, List InProgress, - List Delegated, List Review, List Blocked, List Done diff --git a/backend/Services/TaskService.cs b/backend/Services/TaskService.cs index 09cf67f..5311d9a 100644 --- a/backend/Services/TaskService.cs +++ b/backend/Services/TaskService.cs @@ -161,8 +161,8 @@ public sealed class TaskService( /// /// Returns agent-tasks grouped by which agent is expected to respond, - /// with stale-detection: tasks in InProgress/Delegated that haven't been - /// updated within the stale threshold. + /// with stale-detection: parent tasks that remain in progress while child work + /// is active, and any in-progress task that has not been updated within the stale threshold. /// public async Task GetAgentWorkflowOverviewAsync(TimeSpan staleThreshold, CancellationToken ct = default) { @@ -194,8 +194,7 @@ public sealed class TaskService( var staleTasks = map(agentTasks .Where(t => - (string.Equals(t.State, "In progress", StringComparison.OrdinalIgnoreCase) || - string.Equals(t.State, "Delegated", StringComparison.OrdinalIgnoreCase)) && + string.Equals(t.State, "In progress", StringComparison.OrdinalIgnoreCase) && t.UpdatedAt < threshold)); return new AgentWorkflowOverview(waitingForBao, waitingForIris, waitingForOthers, @@ -214,14 +213,18 @@ public sealed class TaskService( throw new ArgumentException($"Parent task {parentTaskId} not found.", nameof(parentTaskId)); } + var normalizedSource = string.IsNullOrWhiteSpace(source) ? "bao" : source.Trim().ToLowerInvariant(); + var normalizedAssignee = ValidateAssignedTo(assignedTo); + var task = new WorkTask { Title = title.Trim(), Detail = detail?.Trim(), - Source = string.IsNullOrWhiteSpace(source) ? "bao" : source.Trim(), + Source = normalizedSource, Priority = string.IsNullOrWhiteSpace(priority) ? "Normal" : priority.Trim(), - AssignedTo = ValidateAssignedTo(assignedTo), - ParentTaskId = parentTaskId + AssignedTo = normalizedAssignee, + ParentTaskId = parentTaskId, + IsAgentTask = parentTaskId.HasValue }; await taskRepo.AddAsync(task, ct); @@ -231,7 +234,7 @@ public sealed class TaskService( await activityRepo.AddAsync(new ActivityEvent { Type = "task", Message = message, TaskId = task.Id }, ct); // Auto-notify: if assigned to bao, create a task_assigned notification - if (string.Equals(assignedTo, "bao", StringComparison.OrdinalIgnoreCase)) + if (string.Equals(normalizedAssignee, "bao", StringComparison.OrdinalIgnoreCase)) { await notificationService.CreateAsync( "task_assigned", @@ -253,6 +256,7 @@ public sealed class TaskService( task.IsAgentTask = true; task.ExpectedFrom = string.IsNullOrWhiteSpace(expectedFrom) ? null : expectedFrom.Trim().ToLowerInvariant(); + task.State = TaskStateHelper.ToStateString(TaskState.InProgress); // Persist the agent-task-specific fields await taskRepo.UpdateAsync(task, ct); @@ -407,7 +411,6 @@ public sealed class TaskService( var all = await taskRepo.GetAllAsync(ct); var offen = new List(); var inProgress = new List(); - var delegated = new List(); var review = new List(); var blocked = new List(); var done = new List(); @@ -421,8 +424,6 @@ public sealed class TaskService( offen.Add(dto); break; case "in progress": inProgress.Add(dto); break; - case "delegated": - delegated.Add(dto); break; case "review": review.Add(dto); break; case "blocked": @@ -436,12 +437,11 @@ public sealed class TaskService( offen.Sort(SortByPriorityThenCreatedAt); inProgress.Sort(SortByPriorityThenCreatedAt); - delegated.Sort(SortByPriorityThenCreatedAt); review.Sort(SortByPriorityThenCreatedAt); blocked.Sort(SortByPriorityThenCreatedAt); done.Sort(SortByPriorityThenCreatedAt); - return new BoardResponse(offen, inProgress, delegated, review, blocked, done); + return new BoardResponse(offen, inProgress, review, blocked, done); } private static int SortByPriorityThenCreatedAt(DashboardTaskDto a, DashboardTaskDto b) @@ -500,8 +500,7 @@ public sealed class TaskService( var all = await taskRepo.GetAllAsync(ct); var threshold = DateTimeOffset.UtcNow - staleThreshold; var staleTasks = all.Where(t => - (string.Equals(t.State, "In progress", StringComparison.OrdinalIgnoreCase) || - string.Equals(t.State, "Delegated", StringComparison.OrdinalIgnoreCase)) && + string.Equals(t.State, "In progress", StringComparison.OrdinalIgnoreCase) && t.UpdatedAt < threshold).ToList(); foreach (var task in staleTasks) diff --git a/docs/openclaw-task-board-flow.md b/docs/openclaw-task-board-flow.md new file mode 100644 index 0000000..164b0de --- /dev/null +++ b/docs/openclaw-task-board-flow.md @@ -0,0 +1,320 @@ +# OpenClaw ↔ Nexus Task Board Flow + +> Letzte Aktualisierung: 2026-06-21 +> Status: kanonische Arbeitsbeschreibung für Iris, Sub-Agenten und das Nexus Task Board + +Diese Datei beschreibt den gewünschten und umgesetzten Arbeitsfluss zwischen: + +- **Bao** als Auftraggeber +- **Iris** als Chief of Staff / Koordinatorin +- **Sub-Agenten** als ausführende Spezialisten +- **OpenClaw** als Agent-Runtime +- **Nexus Task Board** als sichtbare Aufgabenquelle + +--- + +## 1. Kurzfassung + +**Eine Hauptaufgabe gehört Iris.** +Wenn Iris Arbeit delegiert, wird diese Delegation **nicht unsichtbar im Chat** geführt, sondern als **sichtbare Child-Task** im Nexus Task Board angelegt. + +Das bedeutet: + +- **Parent-Task** = Verantwortung von Iris +- **Child-Task** = konkrete Arbeitsaufgabe für einen Spezial-Agenten +- **Board** = sichtbare Wahrheit für Aufgabenstatus und Ownership +- **OpenClaw** = Ausführungspfad für Agentenarbeit + +--- + +## 2. Die Hauptidee + +Früher war Delegation leicht unsichtbar oder lief über einen separaten `Delegated`-Status. + +Der neue Flow ersetzt das durch: + +1. **Iris übernimmt eine Parent-Task** +2. **Iris zerlegt die Arbeit bei Bedarf in Subtasks** +3. **Jede echte Delegation wird als Child-Task auf dem Board angelegt** +4. **Der zuständige Agent arbeitet gegen diese Child-Task** +5. **Iris integriert die Ergebnisse zurück in die Parent-Task** +6. **Erst wenn alles fertig ist, geht die Parent-Task in Review** + +--- + +## 3. Systembild + +```mermaid +flowchart LR + Bao[Bao\nAuftraggeber] + Iris[Iris\nChief of Staff] + Board[Nexus Task Board\nParent + Child Tasks] + OC[OpenClaw Runtime] + Agents[Sub-Agenten\nDeveloper / Reviewer / Architekt / ...] + + Bao -->|Auftrag / Priorisierung| Iris + Iris -->|legt Parent-Task an / übernimmt Task| Board + Iris -->|delegiert konkrete Arbeit| OC + OC -->|führt Agenten-Task aus| Agents + Iris -->|legt Child-Tasks an| Board + Agents -->|arbeiten gegen Child-Tasks| Board + Agents -->|liefern Ergebnis / melden Blocker| Iris + Iris -->|integriert Ergebnis| Board + Board -->|Review für Bao| Bao +``` + +--- + +## 4. Rollen und Verantwortlichkeiten + +### Bao +- gibt Aufgaben inhaltlich vor +- priorisiert und nimmt fertige Arbeit ab +- verschiebt fertige Hauptaufgaben aus **Review** nach **Done** oder zurück + +### Iris +- übernimmt die Parent-Task +- analysiert, zerlegt, delegiert und reviewed +- hält die Hauptaufgabe auf dem Board aktuell +- erstellt sichtbare Child-Tasks für delegierte Arbeit +- entscheidet, ob etwas **In Progress**, **Blocked** oder **Review** ist + +### Sub-Agenten +- arbeiten **nicht** direkt gegen eine diffuse Hauptaufgabe +- arbeiten gegen eine **konkret zugewiesene Child-Task** +- melden Fortschritt, Ergebnisse und Blocker an Iris + +### OpenClaw +- führt die Agentenarbeit technisch aus +- liefert Nachrichten, Status und Arbeitsergebnisse zurück +- ersetzt nicht das Board als Aufgabenwahrheit + +### Nexus Task Board +- ist die **sichtbare operative Quelle** für Aufgaben +- zeigt Parent-Task, Child-Tasks, Ownership und Status +- dokumentiert den tatsächlichen Arbeitsfluss + +--- + +## 5. Parent-Task vs. Child-Task + +| Ebene | Zweck | Owner | Sichtbarkeit | +|---|---|---|---| +| Parent-Task | Hauptauftrag / Koordination | Iris | Board | +| Child-Task | Delegierter Arbeitsblock | zuständiger Agent | Board | + +### Parent-Task-Regeln +- bleibt bei Iris +- bleibt in der Regel **In Progress**, solange Koordination läuft +- geht erst auf **Review**, wenn alle nötigen Child-Tasks erledigt und integriert sind +- geht nur auf **Blocked**, wenn Iris insgesamt nicht weiterkommt + +### Child-Task-Regeln +- repräsentiert eine echte delegierte Teilaufgabe +- hat klare Ownership (`AssignedTo`) +- zeigt sichtbar, welcher Agent woran arbeitet +- wird nicht für triviale Mini-Schritte missbraucht + +--- + +## 6. Zustandsmodell + +### Parent-Task-Lifecycle + +```mermaid +stateDiagram-v2 + [*] --> Backlog + Backlog --> InProgress: Iris übernimmt + InProgress --> InProgress: Child-Tasks anlegen / koordinieren + InProgress --> Blocked: Gesamtblocker + InProgress --> Review: alles integriert + Review --> Done: Bao nimmt ab + Review --> Backlog: Bao gibt zurück + Blocked --> Backlog: Blocker gelöst +``` + +### Child-Task-Lifecycle + +```mermaid +stateDiagram-v2 + [*] --> Backlog + Backlog --> InProgress: Agent startet + InProgress --> Done: Ergebnis geliefert + InProgress --> Blocked: Agent kommt nicht weiter + Blocked --> Backlog: neu geplant / entsperrt + Blocked --> InProgress: Iris stößt Weiterarbeit an +``` + +--- + +## 7. Der konkrete Arbeitsablauf + +### Fall A: Bao gibt Iris einen neuen Auftrag + +1. Bao formuliert einen Auftrag +2. Iris prüft Ziel, Scope, Risiko und Umgebung +3. Iris übernimmt oder erstellt die **Parent-Task** +4. Parent-Task geht auf **In Progress** +5. Wenn nötig zerlegt Iris die Arbeit in **Child-Tasks** +6. Child-Tasks werden passenden Agenten zugewiesen +7. Agenten arbeiten die Child-Tasks ab +8. Iris sammelt Ergebnisse ein und integriert sie +9. Parent-Task geht auf **Review** +10. Bao entscheidet: **Done** oder zurück nach **Backlog** + +### Fall B: Agent meldet einen Blocker + +1. Agent meldet Blocker an Iris +2. Iris prüft, ob der Blocker lokal lösbar ist +3. Wenn nein: die betroffene **Child-Task** geht auf **Blocked** +4. Falls nötig entsteht eine neue Ursachen-Task / neue Child-Task +5. Parent-Task bleibt **In Progress**, solange der Gesamtauftrag noch koordiniert wird +6. Nur wenn die Hauptaufgabe insgesamt feststeckt, geht die **Parent-Task** auf **Blocked** + +--- + +## 8. OpenClaw- und Board-Interaktion + +```mermaid +sequenceDiagram + participant Bao + participant Iris + participant Board as Nexus Task Board + participant OpenClaw + participant Agent as Sub-Agent + + Bao->>Iris: Auftrag + Iris->>Board: Parent-Task übernehmen / anlegen + Iris->>Board: Child-Task anlegen + Iris->>OpenClaw: Agentenauftrag starten + OpenClaw->>Agent: Task ausführen + Agent-->>Iris: Ergebnis / Rückfrage / Blocker + Iris->>Board: Child-Task aktualisieren + Iris->>Board: Parent-Task integrieren + Iris->>Board: Parent auf Review setzen + Board-->>Bao: Review sichtbar +``` + +--- + +## 9. Regeln für gutes Schneiden von Child-Tasks + +Eine Child-Task ist sinnvoll, wenn sie: + +- einen **klaren Arbeitsblock** darstellt +- einen **eigenen Verantwortlichen** hat +- ein **eigenes Ergebnis** liefern soll +- unabhängig als **Done** oder **Blocked** sichtbar sein kann + +Keine gute Child-Task ist: + +- „Datei öffnen" +- „kurz nachschauen" +- „eine Kleinigkeit prüfen" + +Faustregel: + +> **Eine Child-Task soll ein echter delegierbarer Arbeitsauftrag sein, kein Mikro-Schritt.** + +--- + +## 10. Board-Sicht: was sichtbar sein soll + +Im Board soll erkennbar sein: + +- welche Parent-Task Iris gerade steuert +- welche Child-Tasks darunter existieren +- welcher Agent welche Child-Task besitzt +- welche Child-Task blockiert ist +- welche Parent-Task in Review auf Bao wartet + +Im Task-Detail sollen sichtbar sein: + +- Parent/Child-Beziehung +- `AssignedTo` +- Status +- erwarteter nächster Beitrag / letzter Aktivitätshinweis +- Child-Task-Liste direkt unter der Parent-Task + +--- + +## 11. Kanonische Regeln + +### Regel 1 — Das Board ist die sichtbare Aufgabenwahrheit +Chat und Agentenläufe ergänzen das Board, ersetzen es aber nicht. + +### Regel 2 — Iris bleibt Ownerin der Hauptaufgabe +Delegation verschiebt Verantwortung nicht automatisch auf den Agenten. + +### Regel 3 — Delegation ist sichtbar +Jede echte delegierte Arbeit wird als Child-Task abgebildet. + +### Regel 4 — Kein künstlicher Wartezustand auf Parent-Ebene +Die Parent-Task bleibt **In Progress**, solange Iris aktiv koordiniert. + +### Regel 5 — Blocker präzise markieren +Wenn nur ein Arbeitspaket hängt, blockiert zuerst die **Child-Task**, nicht automatisch die ganze Parent-Task. + +### Regel 6 — Review ist Bao-Gate +Fertige Hauptaufgaben gehen erst in **Review**, dann nach Bao-Entscheid auf **Done** oder zurück. + +--- + +## 12. Beispiel + +### Parent-Task +**„Nexus Taskflow auf Parent-/Child-Modell umstellen“** — Owner: `iris` + +### Mögliche Child-Tasks +- **Backend-State-Handling anpassen** — Owner: `developer` +- **Frontend-Board-Spalten und Labels anpassen** — Owner: `developer` +- **Workflow verifizieren / Regression prüfen** — Owner: `reviewer` +- **Deploy-/Runtime-Auswirkung prüfen** — Owner: `architekt` + +So sieht Bao später nicht nur „Iris arbeitet daran“, sondern konkret: + +- welcher Teil erledigt ist +- welcher Teil noch läuft +- welcher Teil blockiert ist +- worauf Iris gerade wartet + +--- + +## 13. Anti-Patterns + +Diese Muster sollen vermieden werden: + +- Parent-Task auf einen bloßen **Delegated**-Wartestatus schieben +- Delegation nur im Chat sichtbar machen +- Child-Tasks ohne klare Ownership anlegen +- Blocker nur mündlich erwähnen, aber nicht im Board markieren +- zehn Mikro-Subtasks für einen Mini-Arbeitsschritt erzeugen + +--- + +## 14. Entscheidungsregel für Iris + +Wenn Iris unsicher ist, ob sie eine Child-Task anlegen soll, gilt: + +**Child-Task anlegen**, wenn mindestens einer der Punkte zutrifft: + +- anderer Agent übernimmt echte Arbeit +- eigener Status muss sichtbar verfolgt werden +- eigener Blocker ist möglich +- Bao soll Transparenz über diesen Teil sehen + +--- + +## 15. Technische Leitplanken + +- `parentTaskId` verknüpft Child-Tasks mit der Parent-Task +- `AssignedTo` zeigt den operativen Owner +- Agentenstatus und Boardstatus dürfen sich ergänzen, aber nicht widersprechen +- Board-Spalten und API-State-Mapping müssen das Parent-/Child-Modell sauber abbilden +- UI und Doku müssen dieselbe Sprache sprechen + +--- + +## 16. Merksatz + +> **Iris koordiniert die Hauptaufgabe. Agenten erledigen sichtbare Child-Tasks. Das Board zeigt die Wahrheit. OpenClaw führt aus.** diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index e52581c..3dc2c57 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -7,6 +7,8 @@ export async function apiFetch(input: RequestInfo | URL, init: RequestInit = {}) const send = () => { const headers = new Headers(init.headers) if (auth.accessToken) headers.set('Authorization', `Bearer ${auth.accessToken}`) + if (auth.isIris) headers.set('X-Agent-Id', 'iris') + else if (auth.isBao) headers.set('X-Agent-Id', 'bao') return fetch(input, { ...init, headers, credentials: 'include' }) } diff --git a/frontend/src/stores/tasks.ts b/frontend/src/stores/tasks.ts index 53858d1..03d0cf7 100644 --- a/frontend/src/stores/tasks.ts +++ b/frontend/src/stores/tasks.ts @@ -4,7 +4,7 @@ * Fetches tasks from /api/dashboard/tasks and /api/dashboard/tasks/board * and maps them into TaskItem[] format for the TaskStrip component. * - * Board state: grouped by column (offen, inProgress, delegated, review, done, blocked) + * Board state: grouped by column (offen, inProgress, review, blocked, done) * Auto-refresh: every 30 seconds. */ import { defineStore } from 'pinia' @@ -34,10 +34,9 @@ export interface DashboardTaskDto { export interface BoardGroup { offen: DashboardTaskDto[] inProgress: DashboardTaskDto[] - delegated: DashboardTaskDto[] review: DashboardTaskDto[] - done: DashboardTaskDto[] blocked: DashboardTaskDto[] + done: DashboardTaskDto[] } export interface AgentWorkflowOverview { @@ -97,10 +96,9 @@ export const useTaskStore = defineStore('tasks', { board: { offen: [] as DashboardTaskDto[], inProgress: [] as DashboardTaskDto[], - delegated: [] as DashboardTaskDto[], review: [] as DashboardTaskDto[], - done: [] as DashboardTaskDto[], blocked: [] as DashboardTaskDto[], + done: [] as DashboardTaskDto[], } as BoardGroup, boardLoading: false, boardError: null as string | null, @@ -169,10 +167,9 @@ export const useTaskStore = defineStore('tasks', { const canonicalMap: Record = { offen: 'Backlog', inProgress: 'In progress', - delegated: 'Delegated', review: 'Review', - done: 'Done', blocked: 'Blocked', + done: 'Done', } // Save previous state for rollback @@ -188,7 +185,6 @@ export const useTaskStore = defineStore('tasks', { const task = findAndRemove(this.board.offen) ?? findAndRemove(this.board.inProgress) ?? - findAndRemove(this.board.delegated) ?? findAndRemove(this.board.review) ?? findAndRemove(this.board.blocked) ?? findAndRemove(this.board.done) @@ -322,6 +318,7 @@ export const useTaskStore = defineStore('tasks', { priority?: string assignedTo?: string expectedFrom?: string + parentTaskId?: string | null }) { try { const res = await apiFetch('/api/dashboard/tasks/agent', { @@ -333,6 +330,7 @@ export const useTaskStore = defineStore('tasks', { priority: data.priority ?? 'Medium', assignedTo: data.assignedTo ?? null, expectedFrom: data.expectedFrom ?? null, + parentTaskId: data.parentTaskId ?? null, }), }) if (!res.ok) throw new Error(`HTTP ${res.status}`) diff --git a/frontend/src/types/dashboard.ts b/frontend/src/types/dashboard.ts index 06843cd..8ac1fdf 100644 --- a/frontend/src/types/dashboard.ts +++ b/frontend/src/types/dashboard.ts @@ -37,8 +37,8 @@ export interface RoutingTarget { detail: string } -export type TaskState = 'Backlog' | 'In progress' | 'Blocked' | 'Done' -export const TASK_STATES: TaskState[] = ['Backlog', 'In progress', 'Blocked', 'Done'] +export type TaskState = 'Backlog' | 'In progress' | 'Review' | 'Blocked' | 'Done' +export const TASK_STATES: TaskState[] = ['Backlog', 'In progress', 'Review', 'Blocked', 'Done'] export interface OperationsSnapshot { generatedAt: string diff --git a/frontend/src/views/TaskBoardView.vue b/frontend/src/views/TaskBoardView.vue index 5b77dad..623b956 100644 --- a/frontend/src/views/TaskBoardView.vue +++ b/frontend/src/views/TaskBoardView.vue @@ -3,13 +3,13 @@ * TaskBoardView – Linear-style Kanban Board * Galaxy/Dashboard V2 styled edition. * - * 6 columns: Offen, In Bearbeitung, Delegiert, Review, Blockiert, Erledigt + * 5 columns: Offen, In Bearbeitung, Review, Blockiert, Erledigt * HTML5 Drag & Drop (no external lib) * * Agent-Workflow Features: * - Agent-Tasks have a 🤖 badge * - ExpectedFrom field shows who is expected to act next - * - Stale-task warning banner at top (InProgress/Delegated > 2h) + * - Stale-task warning banner at top (InProgress > 2h) * - Waiting section for Iris overview */ import { computed, onBeforeUnmount, onMounted, onUnmounted, reactive, ref, watch } from 'vue' @@ -157,7 +157,6 @@ function statusTone(state: string): string { case 'done': return 'is-done' case 'blocked': return 'is-blocked' case 'review': return 'is-review' - case 'delegated': return 'is-delegated' case 'in progress': return 'is-progress' default: return 'is-backlog' } @@ -183,7 +182,6 @@ function flattenBoard() { return [ ...taskStore.board.offen, ...taskStore.board.inProgress, - ...taskStore.board.delegated, ...taskStore.board.review, ...taskStore.board.blocked, ...taskStore.board.done, @@ -250,8 +248,45 @@ function relativeTime(date?: string | null): string { return `vor ${days} d` } +function childStatusSummary(taskId: string): string { + const children = allBoardTasks.value.filter(task => task.parentTaskId === taskId) + if (!children.length) return '' + + const counts = { + inProgress: children.filter(task => task.state === 'In progress').length, + review: children.filter(task => task.state === 'Review').length, + blocked: children.filter(task => task.state === 'Blocked').length, + done: children.filter(task => task.state === 'Done').length, + } + + const parts = [] as string[] + if (counts.inProgress) parts.push(`${counts.inProgress} in Arbeit`) + if (counts.review) parts.push(`${counts.review} im Review`) + if (counts.blocked) parts.push(`${counts.blocked} blockiert`) + if (counts.done) parts.push(`${counts.done} erledigt`) + return parts.length ? `Child-Tasks: ${parts.join(' · ')}` : `Child-Tasks: ${children.length}` +} + function activityHint(task: BoardTask): string { - return task.lastActivityMessage?.trim() || (task.expectedFrom ? `Wartet auf ${expectedFromLabel(task.expectedFrom)}` : 'Noch kein relevanter Progress-Status') + return task.lastActivityMessage?.trim() + || childStatusSummary(task.id) + || (task.expectedFrom ? `Wartet auf ${expectedFromLabel(task.expectedFrom)}` : 'Noch kein relevanter Progress-Status') +} + +function hasChildTasks(taskId: string): boolean { + return allBoardTasks.value.some(task => task.parentTaskId === taskId) +} + +function assigneeLabel(assignedTo: string | null | undefined): string { + return expectedFromLabel(assignedTo) +} + +function assigneeClass(assignedTo: string | null | undefined): string { + if (!assignedTo) return '' + const lower = assignedTo.toLowerCase() + if (lower === 'iris') return 'assignee-iris' + if (lower === 'bao') return 'assignee-bao' + return 'assignee-agent' } /* ── Task Navigation ───────────────────────────── */ @@ -399,7 +434,7 @@ onUnmounted(() => {
- {{ staleCount }} Task(s) sind stale (InBearbeitung/Delegiert > 2h ohne Update). + {{ staleCount }} Task(s) sind stale (In Bearbeitung > 2h ohne Update).
@@ -514,14 +549,14 @@ onUnmounted(() => { - {{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }} + {{ assigneeLabel(task.assignedTo) }}
{{ task.title }}
{{ task.detail }}
-
{{ activityHint(task) }}
+
{{ activityHint(task) }}
Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}
Keine Aufgaben
@@ -563,69 +598,20 @@ onUnmounted(() => { - {{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }} + {{ assigneeLabel(task.assignedTo) }}
{{ task.title }}
{{ task.detail }}
-
{{ activityHint(task) }}
+
{{ activityHint(task) }}
Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}
Keine Aufgaben
-
-
- - Delegiert - {{ taskStore.board.delegated.length }} -
-
- -
Keine delegierten Aufgaben
-
-
-
{ - {{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }} + {{ assigneeLabel(task.assignedTo) }}
{{ task.title }}
{{ task.detail }}
-
{{ activityHint(task) }}
+
{{ activityHint(task) }}
Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}
Keine Aufgaben
@@ -705,14 +691,14 @@ onUnmounted(() => { - {{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }} + {{ assigneeLabel(task.assignedTo) }}
{{ task.title }}
{{ task.detail }}
-
{{ activityHint(task) }}
+
{{ activityHint(task) }}
Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}
Keine Aufgaben
@@ -754,13 +740,14 @@ onUnmounted(() => { - {{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }} + {{ assigneeLabel(task.assignedTo) }}
{{ task.title }}
{{ task.detail }}
+
{{ activityHint(task) }}
Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}
Keine Blockierer
@@ -849,11 +836,12 @@ onUnmounted(() => { #{{ selectedTask.id.slice(0, 8) }} 🤖 Agent-Task ⏳ Erwartet: {{ selectedTask.expectedFrom }} + ↳ Child-Task Aktualisiert {{ formatDate(selectedTask.updatedAt, true) }} Erstellt {{ formatDate(selectedTask.createdAt) }} Letzter Status {{ relativeTime(selectedTask.lastActivityAt ?? selectedTask.updatedAt) }} -
+
Letzter Fortschritt: {{ activityHint(selectedTask) }}
@@ -914,7 +902,6 @@ onUnmounted(() => { > - @@ -970,6 +957,10 @@ onUnmounted(() => {
Erwartet von
{{ selectedTask.expectedFrom }}
+
+
Task-Typ
+
Sichtbare Child-Task
+
@@ -1078,6 +1069,7 @@ select:disabled { opacity: .45; cursor: not-allowed; } .assignee { font-family: 'Manrope', sans-serif; font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; } .assignee-iris { background: rgba(147, 51, 234, .12); color: #c084fc; } .assignee-bao { background: rgba(59, 130, 246, .12); color: #60a5fa; } +.assignee-agent { background: rgba(16, 185, 129, .12); color: #6ee7b7; } .card-title { font-size: 12.5px; font-weight: 600; color: var(--tx); line-height: 1.4; word-break: break-word; font-family: 'Manrope', sans-serif; } .card-preview { margin-top: 6px; font-size: 11px; line-height: 1.45; color: var(--tx-2); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .card-progress-hint { margin-top: 7px; font-size: 10.5px; color: var(--tx-2); line-height: 1.4; padding: 6px 8px; border-radius: 8px; background: rgba(124,108,255,.07); border: 1px solid rgba(124,108,255,.12); } @@ -1123,7 +1115,6 @@ select:disabled { opacity: .45; cursor: not-allowed; } .detail-state-pill { width: fit-content; border-radius: 999px; padding: 5px 10px; font-size: 11px; font-weight: 700; letter-spacing: .03em; border: 1px solid transparent; } .detail-state-pill.is-backlog { color: #fde68a; background: rgba(251,191,36,.12); border-color: rgba(251,191,36,.25); } .detail-state-pill.is-progress { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); } -.detail-state-pill.is-delegated { color: #d8b4fe; background: rgba(168,85,247,.12); border-color: rgba(168,85,247,.25); } .detail-state-pill.is-review { color: #fdba74; background: rgba(249,115,22,.12); border-color: rgba(249,115,22,.25); } .detail-state-pill.is-blocked { color: #fda4af; background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.25); } .detail-state-pill.is-done { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); } diff --git a/frontend/src/views/TaskDetailView.vue b/frontend/src/views/TaskDetailView.vue index ec29bac..b3cd8b4 100644 --- a/frontend/src/views/TaskDetailView.vue +++ b/frontend/src/views/TaskDetailView.vue @@ -92,7 +92,6 @@ function statusLabel(state: string): string { const map: Record = { 'Backlog': 'Offen', 'In progress': 'In Bearbeitung', - 'Delegated': 'Delegiert', 'Review': 'Review', 'Blocked': 'Blockiert', 'Done': 'Erledigt', @@ -105,7 +104,6 @@ function statusClass(state: string): string { if (s === 'done') return 'is-done' if (s === 'blocked') return 'is-blocked' if (s === 'review') return 'is-review' - if (s === 'delegated') return 'is-delegated' if (s === 'in progress') return 'is-progress' return 'is-backlog' } @@ -148,8 +146,27 @@ function relativeTime(date?: string | null): string { return `vor ${days} d` } -function progressHint(taskLike: Pick): string { - return taskLike.lastActivityMessage?.trim() || (taskLike.expectedFrom ? `Wartet auf ${taskLike.expectedFrom}` : 'Noch kein relevanter Progress-Status') +function childStatusSummary(taskId: string): string { + const childItems = children.value.filter(child => child.parentTaskId === taskId) + if (!childItems.length) return '' + + const counts = { + inProgress: childItems.filter(child => child.state === 'In progress').length, + review: childItems.filter(child => child.state === 'Review').length, + blocked: childItems.filter(child => child.state === 'Blocked').length, + done: childItems.filter(child => child.state === 'Done').length, + } + + const parts = [] as string[] + if (counts.inProgress) parts.push(`${counts.inProgress} in Arbeit`) + if (counts.review) parts.push(`${counts.review} im Review`) + if (counts.blocked) parts.push(`${counts.blocked} blockiert`) + if (counts.done) parts.push(`${counts.done} erledigt`) + return parts.length ? `Child-Tasks: ${parts.join(' · ')}` : `Child-Tasks: ${childItems.length}` +} + +function progressHint(taskLike: Pick): string { + return taskLike.lastActivityMessage?.trim() || childStatusSummary(taskLike.id) || (taskLike.expectedFrom ? `Wartet auf ${taskLike.expectedFrom}` : 'Noch kein relevanter Progress-Status') } /* ── API calls ───────────────────────────────── */ @@ -362,6 +379,7 @@ function handleKeydown(e: KeyboardEvent) { {{ task.priority }} Priorität 🤖 Agent-Task ⏳ Erwartet: {{ task.expectedFrom }} + ↳ Sichtbare Child-Task @@ -390,7 +408,7 @@ function handleKeydown(e: KeyboardEvent) { -
+
Letzter Fortschritt: {{ progressHint(task) }}
@@ -424,7 +442,7 @@ function handleKeydown(e: KeyboardEvent) { - + @@ -455,6 +473,8 @@ function handleKeydown(e: KeyboardEvent) { > + +