From 3aacd4385eeb2f08cf0257d9a85df0285f5ce405 Mon Sep 17 00:00:00 2001 From: Hesham Salman Date: Fri, 20 Mar 2026 23:09:44 -0400 Subject: [PATCH] fix: reconcile pipeline task counts on team completion UpdateTeamCompleted overwrote TasksDone/TasksFailed with backend- authoritative values but left TasksTotal at the stale incremental count from bridge start events, causing "exec 3/2" when the backend reported more completed tasks than bridge had tracked starts. Now sets TasksTotal = tasksDone + tasksFailed and clears ActiveTasks on completion, matching the existing logic in the "team not yet tracked" branch. --- CHANGELOG.md | 1 + internal/tui/view/pipeline_status.go | 6 ++-- internal/tui/view/pipeline_status_test.go | 34 +++++++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa24600f..37579bb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Subprocess Mode** - Removed the experimental subprocess execution mode (`experimental.subprocess_mode`), including the `internal/streamjson/` package, `subprocessFactory`, and all related config/TUI/wiring plumbing. Pipeline instances now always use the tmux-based execution backend. ### Fixed +- **Pipeline Execution Count Exceeds Total** - Fixed `exec 3/2` display bug where the task done count exceeded the total count. `UpdateTeamCompleted` overwrote `TasksDone`/`TasksFailed` with backend-authoritative values but left `TasksTotal` at the stale incremental count from bridge start events. Now reconciles `TasksTotal` and clears `ActiveTasks` on team completion. - **Ultraplan h/l Navigation Reversed** - Fixed `h` and `l` keybindings navigating in the opposite visual direction in ultraplan mode with groups. Navigation used plan-execution order (`getNavigableInstances`) while the sidebar rendered in group-structure order (`FlattenGroupsForDisplay`), causing the two orderings to diverge. Navigation now follows the visual display order filtered to navigable instances. - **Silent Plan Validation Failure** - Fixed `handlePlanFileCheckResult` silently swallowing `SetPlan` errors, leaving users stuck in a session that would never progress. Now sets an error message and transitions to `PhaseFailed`, matching the identical error handling in `handlePlanParsed`. - **Missing Sentinel File in Pipeline Execution** - Fixed task instances not writing `.claudio-task-complete.json` in the Orchestration 2.0 pipeline path. The bridge's `BuildTaskPrompt` relied solely on `--append-system-prompt-file` to inject the completion protocol, which left instances unaware of the sentinel file convention. The completion protocol is now embedded directly in the task prompt as defense-in-depth. diff --git a/internal/tui/view/pipeline_status.go b/internal/tui/view/pipeline_status.go index eef1a1ab..9c840e84 100644 --- a/internal/tui/view/pipeline_status.go +++ b/internal/tui/view/pipeline_status.go @@ -19,10 +19,6 @@ var ( // TeamSnapshot holds a point-in-time snapshot of a team's status. // This is a TUI-local type with no backend imports. -// -// TasksTotal is an incremental count from bridge start events and may diverge -// from TasksDone+TasksFailed after UpdateTeamCompleted, which overwrites -// TasksDone/TasksFailed with backend-authoritative final counts. type TeamSnapshot struct { ID string Name string @@ -117,6 +113,8 @@ func (p *PipelineState) UpdateTeamCompleted(teamID, teamName string, success boo } p.Teams[i].TasksDone = tasksDone p.Teams[i].TasksFailed = tasksFailed + p.Teams[i].TasksTotal = tasksDone + tasksFailed + p.Teams[i].ActiveTasks = 0 if teamName != "" { p.Teams[i].Name = teamName } diff --git a/internal/tui/view/pipeline_status_test.go b/internal/tui/view/pipeline_status_test.go index 14751a69..46e3a961 100644 --- a/internal/tui/view/pipeline_status_test.go +++ b/internal/tui/view/pipeline_status_test.go @@ -204,6 +204,24 @@ func TestPipelineState_UpdateTeamCompleted(t *testing.T) { } }) + t.Run("reconciles TasksTotal with backend counts", func(t *testing.T) { + p := &PipelineState{ + Phase: "execution", + Teams: []TeamSnapshot{{ + ID: "t1", Phase: "working", + TasksDone: 1, TasksTotal: 2, ActiveTasks: 1, + }}, + } + // Backend reports 3 done — more than bridge tracked starts + p.UpdateTeamCompleted("t1", "", true, 3, 0) + if p.Teams[0].TasksTotal != 3 { + t.Errorf("TasksTotal = %d, want %d (should reconcile with backend)", p.Teams[0].TasksTotal, 3) + } + if p.Teams[0].ActiveTasks != 0 { + t.Errorf("ActiveTasks = %d, want 0 (team is done)", p.Teams[0].ActiveTasks) + } + }) + t.Run("nil safety", func(t *testing.T) { var p *PipelineState p.UpdateTeamCompleted("t1", "n", true, 1, 0) // should not panic @@ -341,6 +359,22 @@ func TestPipelineState_GetIndicator(t *testing.T) { } }) + t.Run("execution label after team completion reconciles counts", func(t *testing.T) { + p := &PipelineState{ + Phase: "execution", + Teams: []TeamSnapshot{{ID: "t1", Phase: "working", TasksTotal: 2}}, + } + // Simulate backend reporting more tasks than bridge tracked + p.UpdateTeamCompleted("t1", "", true, 3, 0) + ind := p.GetIndicator() + if ind == nil { + t.Fatal("GetIndicator() = nil, want non-nil") + } + if ind.Label != "exec 3/3" { + t.Errorf("Label = %q, want %q (done should never exceed total)", ind.Label, "exec 3/3") + } + }) + t.Run("execution phase without tasks shows team count", func(t *testing.T) { p := &PipelineState{ Phase: "execution",