feat: Parent-Child TaskFlow — Delegated durch sichtbare Child-Tasks ersetzt
- 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
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -118,7 +118,7 @@ public class TasksController(ITaskService taskService) : ControllerBase
|
||||
=> Results.Ok(await taskService.GetBoardAsync(ct));
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
|
||||
/// <summary>Valid task-state string values for API validation.</summary>
|
||||
public static readonly string[] AllStates = ["Backlog", "In progress", "Delegated", "Blocked", "Done", "Review"];
|
||||
public static readonly string[] AllStates = ["Backlog", "In progress", "Blocked", "Done", "Review"];
|
||||
|
||||
/// <summary>Convert a TaskState enum to its API string representation.</summary>
|
||||
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.
|
||||
/// </summary>
|
||||
@@ -96,7 +92,7 @@ public static class TaskStateHelper
|
||||
var caller = callerAgent?.Trim().ToLowerInvariant() ?? "";
|
||||
|
||||
// Sub-agents must never move state
|
||||
var subAgents = new HashSet<string> { "programmer", "reviewer", "architekt" };
|
||||
var subAgents = new HashSet<string> { "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",
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
[Table("SeedAudit")]
|
||||
public class SeedAudit
|
||||
{
|
||||
[Key]
|
||||
|
||||
@@ -138,7 +138,6 @@ public sealed record AgentActivityEntry(
|
||||
public sealed record BoardResponse(
|
||||
List<DashboardTaskDto> Offen,
|
||||
List<DashboardTaskDto> InProgress,
|
||||
List<DashboardTaskDto> Delegated,
|
||||
List<DashboardTaskDto> Review,
|
||||
List<DashboardTaskDto> Blocked,
|
||||
List<DashboardTaskDto> Done
|
||||
|
||||
@@ -161,8 +161,8 @@ public sealed class TaskService(
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public async Task<AgentWorkflowOverview> 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<DashboardTaskDto>();
|
||||
var inProgress = new List<DashboardTaskDto>();
|
||||
var delegated = new List<DashboardTaskDto>();
|
||||
var review = new List<DashboardTaskDto>();
|
||||
var blocked = new List<DashboardTaskDto>();
|
||||
var done = new List<DashboardTaskDto>();
|
||||
@@ -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)
|
||||
|
||||
@@ -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.**
|
||||
@@ -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' })
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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}`)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(() => {
|
||||
<!-- Stale Warning Banner -->
|
||||
<div v-if="staleCount > 0" class="stale-banner">
|
||||
<AlertTriangle :size="14" />
|
||||
<span><strong>{{ staleCount }} Task(s)</strong> sind stale (InBearbeitung/Delegiert > 2h ohne Update).</span>
|
||||
<span><strong>{{ staleCount }} Task(s)</strong> sind stale (In Bearbeitung > 2h ohne Update).</span>
|
||||
<button class="stale-dismiss" @click="showIrisPanel = true">Ansehen</button>
|
||||
</div>
|
||||
|
||||
@@ -514,14 +549,14 @@ onUnmounted(() => {
|
||||
<span
|
||||
v-if="task.assignedTo"
|
||||
class="assignee"
|
||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
||||
:class="assigneeClass(task.assignedTo)"
|
||||
>
|
||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
||||
{{ assigneeLabel(task.assignedTo) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-title">{{ task.title }}</div>
|
||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||
</button>
|
||||
<div v-if="!taskStore.board.offen.length" class="empty-col">Keine Aufgaben</div>
|
||||
@@ -563,69 +598,20 @@ onUnmounted(() => {
|
||||
<span
|
||||
v-if="task.assignedTo"
|
||||
class="assignee"
|
||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
||||
:class="assigneeClass(task.assignedTo)"
|
||||
>
|
||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
||||
{{ assigneeLabel(task.assignedTo) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-title">{{ task.title }}</div>
|
||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||
</button>
|
||||
<div v-if="!taskStore.board.inProgress.length" class="empty-col">Keine Aufgaben</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col"
|
||||
:class="{ 'drag-over': dragOverColumn === 'delegated' }"
|
||||
@dragover="onDragOver($event, 'delegated')"
|
||||
@dragleave="onDragLeave"
|
||||
@drop="onDrop($event, 'delegated')"
|
||||
>
|
||||
<div class="col-header">
|
||||
<span class="col-icon" style="background: var(--a-purple); box-shadow: 0 0 0 2px rgba(181,87,246,.25);"></span>
|
||||
<span class="col-name">Delegiert</span>
|
||||
<span class="col-count">{{ taskStore.board.delegated.length }}</span>
|
||||
</div>
|
||||
<div class="col-cards">
|
||||
<button
|
||||
v-for="task in taskStore.board.delegated"
|
||||
:key="task.id"
|
||||
type="button"
|
||||
class="card"
|
||||
:class="{ 'card-agent': task.isAgentTask }"
|
||||
draggable="true"
|
||||
@click="handleCardClick($event, task.id)"
|
||||
@dragstart="onDragStart($event, task.id)"
|
||||
@dragend="onDragEnd"
|
||||
>
|
||||
<div class="card-top">
|
||||
<span class="prio-badge" :style="{ color: priorityColor(task.priority), borderColor: priorityColor(task.priority) }">
|
||||
{{ priorityLabel(task.priority) }}
|
||||
</span>
|
||||
<span v-if="task.isAgentTask" class="agent-badge" title="Agent-Task">🤖</span>
|
||||
<span v-if="task.expectedFrom" class="expected-badge" :title="'Erwartet: ' + task.expectedFrom">
|
||||
⏳ {{ task.expectedFrom }}
|
||||
</span>
|
||||
<span
|
||||
v-if="task.assignedTo"
|
||||
class="assignee"
|
||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
||||
>
|
||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-title">{{ task.title }}</div>
|
||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||
</button>
|
||||
<div v-if="!taskStore.board.delegated.length" class="empty-col">Keine delegierten Aufgaben</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col"
|
||||
:class="{ 'drag-over': dragOverColumn === 'review' }"
|
||||
@@ -661,14 +647,14 @@ onUnmounted(() => {
|
||||
<span
|
||||
v-if="task.assignedTo"
|
||||
class="assignee"
|
||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
||||
:class="assigneeClass(task.assignedTo)"
|
||||
>
|
||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
||||
{{ assigneeLabel(task.assignedTo) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-title">{{ task.title }}</div>
|
||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||
</button>
|
||||
<div v-if="!taskStore.board.review.length" class="empty-col">Keine Aufgaben</div>
|
||||
@@ -705,14 +691,14 @@ onUnmounted(() => {
|
||||
<span
|
||||
v-if="task.assignedTo"
|
||||
class="assignee"
|
||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
||||
:class="assigneeClass(task.assignedTo)"
|
||||
>
|
||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
||||
{{ assigneeLabel(task.assignedTo) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-title">{{ task.title }}</div>
|
||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||
<div v-if="task.isAgentTask" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||
</button>
|
||||
<div v-if="!taskStore.board.done.length" class="empty-col">Keine Aufgaben</div>
|
||||
@@ -754,13 +740,14 @@ onUnmounted(() => {
|
||||
<span
|
||||
v-if="task.assignedTo"
|
||||
class="assignee"
|
||||
:class="task.assignedTo === 'iris' ? 'assignee-iris' : 'assignee-bao'"
|
||||
:class="assigneeClass(task.assignedTo)"
|
||||
>
|
||||
{{ task.assignedTo === 'iris' ? '🤖 Iris' : '👤 Bao' }}
|
||||
{{ assigneeLabel(task.assignedTo) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-title">{{ task.title }}</div>
|
||||
<div v-if="task.detail" class="card-preview">{{ task.detail }}</div>
|
||||
<div v-if="task.isAgentTask || hasChildTasks(task.id)" class="card-progress-hint">{{ activityHint(task) }}</div>
|
||||
<div class="card-meta">Update {{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</div>
|
||||
</button>
|
||||
<div v-if="!taskStore.board.blocked.length" class="empty-col">Keine Blockierer</div>
|
||||
@@ -849,11 +836,12 @@ onUnmounted(() => {
|
||||
<span>#{{ selectedTask.id.slice(0, 8) }}</span>
|
||||
<span v-if="selectedTask.isAgentTask" class="meta-agent-tag">🤖 Agent-Task</span>
|
||||
<span v-if="selectedTask.expectedFrom" class="meta-expected">⏳ Erwartet: {{ selectedTask.expectedFrom }}</span>
|
||||
<span v-if="selectedTask.parentTaskId" class="meta-expected">↳ Child-Task</span>
|
||||
<span><Clock3 :size="13" /> Aktualisiert {{ formatDate(selectedTask.updatedAt, true) }}</span>
|
||||
<span><CalendarDays :size="13" /> Erstellt {{ formatDate(selectedTask.createdAt) }}</span>
|
||||
<span v-if="selectedTask.isAgentTask"><MessageSquareText :size="13" /> Letzter Status {{ relativeTime(selectedTask.lastActivityAt ?? selectedTask.updatedAt) }}</span>
|
||||
</div>
|
||||
<div v-if="selectedTask.isAgentTask" class="detail-progress-banner">
|
||||
<div v-if="selectedTask.isAgentTask || hasChildTasks(selectedTask.id)" class="detail-progress-banner">
|
||||
<strong>Letzter Fortschritt:</strong> {{ activityHint(selectedTask) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -914,7 +902,6 @@ onUnmounted(() => {
|
||||
>
|
||||
<option value="Backlog">Offen</option>
|
||||
<option value="In progress">In Bearbeitung</option>
|
||||
<option value="Delegated">Delegiert</option>
|
||||
<option value="Review">Review</option>
|
||||
<option value="Blocked">Blockiert</option>
|
||||
<option value="Done">Erledigt</option>
|
||||
@@ -970,6 +957,10 @@ onUnmounted(() => {
|
||||
<dt>Erwartet von</dt>
|
||||
<dd>{{ selectedTask.expectedFrom }}</dd>
|
||||
</div>
|
||||
<div v-if="selectedTask.parentTaskId">
|
||||
<dt>Task-Typ</dt>
|
||||
<dd>Sichtbare Child-Task</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -92,7 +92,6 @@ function statusLabel(state: string): string {
|
||||
const map: Record<string, string> = {
|
||||
'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<TaskDto, 'lastActivityMessage' | 'expectedFrom'>): 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<TaskDto, 'id' | 'lastActivityMessage' | 'expectedFrom'>): 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) {
|
||||
<span class="meta-chip">{{ task.priority }} Priorität</span>
|
||||
<span v-if="task.isAgentTask" class="meta-chip">🤖 Agent-Task</span>
|
||||
<span v-if="task.expectedFrom" class="meta-chip">⏳ Erwartet: {{ task.expectedFrom }}</span>
|
||||
<span v-if="task.parentTaskId" class="meta-chip">↳ Sichtbare Child-Task</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -390,7 +408,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="task.isAgentTask" class="progress-banner">
|
||||
<div v-if="task.isAgentTask || childStatusSummary(task.id)" class="progress-banner">
|
||||
<strong>Letzter Fortschritt:</strong> {{ progressHint(task) }}
|
||||
</div>
|
||||
|
||||
@@ -424,7 +442,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="Low">Low</option>
|
||||
</select>
|
||||
<input v-model="subtaskAssign" class="galaxy-input narrow" placeholder="Zuständig (bao, iris…)" />
|
||||
<input v-model="subtaskAssign" class="galaxy-input narrow" placeholder="Zuständig (bao, iris, researcher…)" />
|
||||
<button class="btn-primary btn-sm" @click="createSubtask" :disabled="creatingSubtask">
|
||||
{{ creatingSubtask ? 'Erstelle…' : 'Anlegen' }}
|
||||
</button>
|
||||
@@ -455,6 +473,8 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
>
|
||||
<option value="Backlog">Offen</option>
|
||||
<option value="In progress">In Arbeit</option>
|
||||
<option value="Review">Review</option>
|
||||
<option value="Blocked">Blockiert</option>
|
||||
<option value="Done">Erledigt</option>
|
||||
</select>
|
||||
<button
|
||||
@@ -520,7 +540,6 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
<select v-model="form.state" class="galaxy-input galaxy-select" :disabled="!canChangeState">
|
||||
<option value="Backlog">Offen</option>
|
||||
<option value="In progress">In Bearbeitung</option>
|
||||
<option value="Delegated">Delegiert</option>
|
||||
<option value="Review">Review</option>
|
||||
<option value="Blocked">Blockiert</option>
|
||||
<option value="Done">Erledigt</option>
|
||||
@@ -543,6 +562,8 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
<option value="programmer">🛠 Programmer</option>
|
||||
<option value="reviewer">🔎 Reviewer</option>
|
||||
<option value="architekt">🏛 Architekt</option>
|
||||
<option value="researcher">🔬 Researcher</option>
|
||||
<option value="executor">⚡ Executor</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="sidebar-field">
|
||||
@@ -559,6 +580,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
<div><dt>Erstellt</dt><dd>{{ formatDate(task.createdAt) }}</dd></div>
|
||||
<div><dt>Geändert</dt><dd>{{ formatDate(task.updatedAt, true) }}</dd></div>
|
||||
<div v-if="task.isAgentTask"><dt>Letzter Status</dt><dd>{{ relativeTime(task.lastActivityAt ?? task.updatedAt) }}</dd></div>
|
||||
<div v-if="task.parentTaskId"><dt>Task-Typ</dt><dd>Sichtbare Child-Task</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
@@ -678,7 +700,6 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
|
||||
.state-badge.is-backlog { color: #fde68a; background: rgba(251,191,36,.12); border-color: rgba(251,191,36,.25); }
|
||||
.state-badge.is-progress { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); }
|
||||
.state-badge.is-delegated { color: #d8b4fe; background: rgba(168,85,247,.12); border-color: rgba(168,85,247,.25); }
|
||||
.state-badge.is-review { color: #fdba74; background: rgba(249,115,22,.12); border-color: rgba(249,115,22,.25); }
|
||||
.state-badge.is-blocked { color: #fda4af; background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.25); }
|
||||
.state-badge.is-done { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); }
|
||||
|
||||
+2
-1
@@ -86,7 +86,8 @@
|
||||
|
||||
- 2026-06-20: Nexus-Auth-Persistenz live verifiziert. Owner-Passwort in der produktiven Postgres-DB geprüft, Stack vollständig neu gestartet und anschließend `postgres`, `api` und `web` per `docker:cli compose up -d --force-recreate` neu erstellt, ohne das DB-Volume zu löschen. Ergebnis: `/health/live` blieb healthy, der Passwort-Hash für `vmbao62@hotmail.de` blieb vor und nach Restart/Recreate identisch. Wichtiges Learning: temporäre Passwörter oder Auth-Fixes niemals an Bao weitergeben, bevor der echte Live-Login oder mindestens der persistierte DB-Hash auf dem Zielstack verifiziert ist.
|
||||
- 2026-06-20: Task Board um klickbare Linear-inspirierte Detailansicht erweitert: Board-Karten öffnen jetzt ein strukturiertes Side/Overlay-Detailpanel mit editierbarem Titel, Beschreibung, Status, Priorität, Zuständigkeit und Fälligkeitsdatum sowie geladener Aktivität und Unteraufgaben. `frontend/src/views/TaskBoardView.vue` und `frontend/src/stores/tasks.ts` angepasst. Verifiziert mit `COREPACK_HOME=$PWD/.corepack-home PNPM_HOME=$PWD/.pnpm-home pnpm build`.
|
||||
- 2026-06-19: Task-Board-Doku-Drift behoben: Header-Kommentar in TaskBoardView.vue von "4 columns" auf "6 columns" (Offen, InBearbeitung, Delegiert, Review, Blockiert, Erledigt) korrigiert. tasks.ts-Store-Kopfkommentar um delegated ergänzt.
|
||||
- 2026-06-21: Delegated-/unsichtbares Delegationsmodell auf sichtbaren Parent-Child-Flow umgestellt. `Delegated` aus Board-State, DTOs, Frontend-Spalten und Tests entfernt; Parent-Tasks bleiben bei delegierter Agentenarbeit `In progress`, Child-Tasks laufen sichtbar mit normalen States und `parentTaskId`. Doku in README, Phase 3, Changelog und Controller-Kommentaren angepasst.
|
||||
- 2026-06-19: Task-Board-Doku-Drift behoben; später im Parent-Child-Flow wieder auf 5 sichtbare States ohne separate Delegiert-Spalte umgestellt.
|
||||
- 2026-06-19: Veralteter TODO.md-Import entfernt: `ImportFromIrisTodoAsync` in TaskService.cs, ITaskService.cs und der import-from-iris-todo-API-Endpoint in DashboardController.cs gelöscht. ImportResultDto aus Models/Dashboard.cs entfernt. TODO.md ist abgeschafft, Task Board alleinige Quelle.
|
||||
- 2026-06-19: Backend-Tests erweitert: TaskBoardTests.cs (69 Tests total, +13 neue) decken TaskStateHelper-BoardGroupKey/ToState/BoardGroupToState/DisplayString/AllStates/IsValidState/IsInProgressOrBlocked/IsDoneOrBacklog ab. Backend-Build 0 Errors, Frontend vue-tsc 0 Errors.
|
||||
- 2026-06-16: Program.cs refactored: DI extrahiert in `Extensions/ServiceCollectionExtensions.cs`, Middleware in `Extensions/ApplicationBuilderExtensions.cs`, Helpers in `Helpers/PasswordHelper.cs`. Program.cs von ~200 auf 26 Zeilen reduziert.
|
||||
|
||||
+7
-3
@@ -1,12 +1,16 @@
|
||||
# Phase 3
|
||||
|
||||
> Letzte Aktualisierung: 2026-06-08
|
||||
> Letzte Aktualisierung: 2026-06-21
|
||||
|
||||
- [ ] Office View
|
||||
- [ ] Kalender
|
||||
- [ ] Agent Collaboration Visualisierung
|
||||
- [x] Agent Collaboration Visualisierung
|
||||
- sichtbarer Parent-Child-Flow im Task Board statt unsichtbarer Delegation
|
||||
- Parent bleibt bei Iris in `In progress`, Child-Tasks sind sichtbar ueber `parentTaskId`
|
||||
- Board/UI zeigen Child-Fortschritt auf Karten und im Detailpanel
|
||||
- Ablauf dokumentiert in [`docs/openclaw-task-board-flow.md`](../docs/openclaw-task-board-flow.md)
|
||||
- [ ] Advanced Reporting
|
||||
|
||||
## Fokus
|
||||
|
||||
Phase 3 bleibt bewusst knapp und wird erst beim Start der Arbeit ausgebaut.
|
||||
Parent-Child-Delegation ist im Board umgesetzt. Offene Phase-3-Schwerpunkte sind jetzt Kalender, Office View und spaetere Reporting-/Visualisierungs-Ausbauten.
|
||||
|
||||
Reference in New Issue
Block a user