diff --git a/CHANGELOG.md b/CHANGELOG.md index aa24600..37579bb 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 eef1a1a..9c840e8 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 14751a6..46e3a96 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",