feat: add per-agent logging UI and fix stuck agent issues

Changes:
- Add per-agent log viewer with copy-to-clipboard functionality
  - New AgentLogEntry type for structured log entries
  - Logs stored per-agent in WebSocket state (up to 500 entries)
  - Log modal rendered via React Portal to avoid overflow issues
  - Click log icon on agent card to view full activity history

- Fix agents getting stuck in "failed" state
  - Wrap client context manager in try/except (agent.py)
  - Remove failed agents from UI on error state (useWebSocket.ts)
  - Handle permanently failed features in get_all_complete()

- Add friendlier agent state labels
  - "Hit an issue" → "Trying plan B..."
  - "Retrying..." → "Being persistent..."
  - Softer colors (yellow/orange instead of red)

- Add scheduling scores for smarter feature ordering
  - compute_scheduling_scores() in dependency_resolver.py
  - Features that unblock others get higher priority

- Update CLAUDE.md with parallel mode documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-01-17 14:11:24 +02:00
parent 85f6940a54
commit bf3a6b0b73
10 changed files with 387 additions and 49 deletions

View File

@@ -41,6 +41,7 @@ from api.dependency_resolver import (
would_create_circular_dependency,
are_dependencies_satisfied,
get_blocking_dependencies,
compute_scheduling_scores,
MAX_DEPENDENCIES_PER_FEATURE,
)
@@ -178,7 +179,11 @@ def feature_get_next() -> str:
# Get pending, non-in-progress features
pending = [f for f in all_features if not f.passes and not f.in_progress]
pending.sort(key=lambda f: (f.priority, f.id))
# Sort by scheduling score (higher = first), then priority, then id
all_dicts = [f.to_dict() for f in all_features]
scores = compute_scheduling_scores(all_dicts)
pending.sort(key=lambda f: (-scores.get(f.id, 0), f.priority, f.id))
if not pending:
if any(f.in_progress for f in all_features if not f.passes):
@@ -247,7 +252,11 @@ def _feature_claim_next_internal(attempt: int = 0) -> str:
# Get pending, non-in-progress features
pending = [f for f in all_features if not f.passes and not f.in_progress]
pending.sort(key=lambda f: (f.priority, f.id))
# Sort by scheduling score (higher = first), then priority, then id
all_dicts = [f.to_dict() for f in all_features]
scores = compute_scheduling_scores(all_dicts)
pending.sort(key=lambda f: (-scores.get(f.id, 0), f.priority, f.id))
if not pending:
if any(f.in_progress for f in all_features if not f.passes):
@@ -814,6 +823,7 @@ def feature_get_ready(
passing_ids = {f.id for f in all_features if f.passes}
ready = []
all_dicts = [f.to_dict() for f in all_features]
for f in all_features:
if f.passes or f.in_progress:
continue
@@ -821,8 +831,9 @@ def feature_get_ready(
if all(dep_id in passing_ids for dep_id in deps):
ready.append(f.to_dict())
# Sort by priority
ready.sort(key=lambda f: (f["priority"], f["id"]))
# Sort by scheduling score (higher = first), then priority, then id
scores = compute_scheduling_scores(all_dicts)
ready.sort(key=lambda f: (-scores.get(f["id"], 0), f["priority"], f["id"]))
return json.dumps({
"features": ready[:limit],