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).