feat(board): master-task board, non-destructive stall watchdog, review flow
CI - Build & Test / Backend (.NET) (push) Successful in 32s
CI - Build & Test / Frontend (Vue/TS) (push) Successful in 18s
CI - Build & Test / Security Check (push) Successful in 2s
CI - Build & Test / Deploy Nexus (push) Has been skipped

Board is now a clean master-task view:
- GetBoardAsync returns only top-level (master) tasks; child-tasks render
  nested inside their parent card instead of as separate column cards, so a
  big task split into many sub-tasks stays one card (orphans treated as master)
- New DoneChildTaskCount on the DTO for real progress bars
- Child/detail consumers (GetChildren endpoint, TaskBridgeService) query
  children directly instead of scraping the flat board

Stall watchdog (replaces destructive auto-reset):
- StaleTaskRecoveryService.FlagStalledInProgressTasksAsync marks In-progress
  tasks with no activity past the threshold as stalled (activity event +
  Iris notification) WITHOUT resetting the column — no work is discarded.
  Idempotent: a task is not re-flagged until real progress happens
- BackgroundService now runs this watchdog (TaskRecovery:StalledMinutes=40,
  interval 10m); hard reset kept only on the explicit manual endpoint

Review flow (Bao/Iris only):
- POST tasks/{id}/approve (Review -> Done)
- POST tasks/{id}/request-changes (Review -> target, mandatory comment,
  ExpectedFrom=iris, notifies Iris)

Frontend:
- BoardCard component: master card with ball chip (who has it), progress from
  children, expand to show children grouped by agent with per-child state +
  stalled marker, stalled chip on the master, review action buttons
- Request-changes modal; tasks store approveReview/requestChanges actions

Tests: watchdog flag/idempotency + review threshold; 135 backend tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-10 22:26:50 +02:00
parent f564ecfbc7
commit aef76d5f45
15 changed files with 789 additions and 432 deletions
@@ -13,9 +13,9 @@ public sealed class StaleTaskRecoveryBackgroundService(
{
try
{
var resetCount = await RunRecoveryOnceAsync(stoppingToken);
if (resetCount > 0)
logger.LogInformation("Stale task recovery reset {ResetCount} task(s).", resetCount);
var flaggedCount = await RunWatchdogOnceAsync(stoppingToken);
if (flaggedCount > 0)
logger.LogInformation("Stall watchdog flagged {FlaggedCount} stalled task(s) for Iris.", flaggedCount);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
@@ -37,10 +37,10 @@ public sealed class StaleTaskRecoveryBackgroundService(
}
}
public async Task<int> RunRecoveryOnceAsync(CancellationToken ct = default)
public async Task<int> RunWatchdogOnceAsync(CancellationToken ct = default)
{
await using var scope = scopeFactory.CreateAsyncScope();
var recoveryService = scope.ServiceProvider.GetRequiredService<IStaleTaskRecoveryService>();
return await recoveryService.ResetStaleInProgressTasksAsync(optionsMonitor.CurrentValue.GetStaleThreshold(), ct);
return await recoveryService.FlagStalledInProgressTasksAsync(optionsMonitor.CurrentValue.GetStalledThreshold(), ct);
}
}