fix(lifecycle): propagate RepositoryName through PublishGitStatus

The agentctl WorkspaceTracker stamps every per-repo GitStatusUpdate
with RepositoryName, but the lifecycle PublishGitStatus translation
into GitEventPayload was dropping that field. The orchestrator then
forwarded a status with empty repository_name, the frontend store
routed everything into the empty-key bucket, and the Changes panel's
multi-repo grouping never triggered — no header, no per-repo split.

- GitStatusData gains RepositoryName (omitempty so single-repo
  payloads stay byte-identical on the wire).
- PublishGitStatus copies update.RepositoryName onto the payload.
- Test asserts the field survives the publish → subscribe round trip
  via the in-memory event bus on the GitEvent.<sessionID> subject.

Restart the agentctl process for any running multi-repo task to pick
up the new field; new launches get it automatically.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Kandev Agent
2026-04-26 10:34:15 +01:00
parent 93d5398311
commit efd8a07c01
3 changed files with 67 additions and 0 deletions
@@ -225,6 +225,10 @@ type GitStatusData struct {
Files interface{} `json:"files,omitempty"`
BranchAdditions int `json:"branch_additions,omitempty"`
BranchDeletions int `json:"branch_deletions,omitempty"`
// RepositoryName identifies which repository this status belongs to in
// multi-repo task workspaces. Empty for single-repo. Carried through to
// the frontend so the Changes panel can render per-repo group headers.
RepositoryName string `json:"repository_name,omitempty"`
}
type GitCommitData struct {
@@ -247,6 +247,7 @@ func (p *EventPublisher) PublishGitStatus(execution *AgentExecution, update *age
Files: update.Files,
BranchAdditions: update.BranchAdditions,
BranchDeletions: update.BranchDeletions,
RepositoryName: update.RepositoryName,
},
})
}
@@ -0,0 +1,62 @@
package lifecycle
import (
"context"
"testing"
"time"
agentctl "github.com/kandev/kandev/internal/agentctl/client"
"github.com/kandev/kandev/internal/common/logger"
"github.com/kandev/kandev/internal/events"
"github.com/kandev/kandev/internal/events/bus"
)
// Verifies the fix for "Changes panel shows no header / no existing changes".
// agentctl tags every per-repo GitStatusUpdate with RepositoryName; that field
// must survive the lifecycle PublishGitStatus translation so the orchestrator
// (and thus the frontend) sees it.
func TestPublishGitStatus_PropagatesRepositoryName(t *testing.T) {
log, _ := logger.NewLogger(logger.LoggingConfig{Level: "error", Format: "json"})
eventBus := bus.NewMemoryEventBus(log)
pub := NewEventPublisher(eventBus, log)
received := make(chan *bus.Event, 1)
subj := events.BuildGitEventSubject("sess-multi")
sub, err := eventBus.Subscribe(subj, func(_ context.Context, ev *bus.Event) error {
received <- ev
return nil
})
if err != nil {
t.Fatalf("subscribe: %v", err)
}
defer func() { _ = sub.Unsubscribe() }()
exec := &AgentExecution{
ID: "exec-1",
TaskID: "task-1",
SessionID: "sess-multi",
}
pub.PublishGitStatus(exec, &agentctl.GitStatusUpdate{
Timestamp: time.Now(),
RepositoryName: "frontend",
Branch: "feature/x",
Modified: []string{"src/app.tsx"},
Files: map[string]agentctl.FileInfo{"src/app.tsx": {Path: "src/app.tsx"}},
})
select {
case ev := <-received:
payload, ok := ev.Data.(*GitEventPayload)
if !ok || payload == nil {
t.Fatalf("expected *GitEventPayload, got %T", ev.Data)
}
if payload.Status == nil {
t.Fatal("expected non-nil Status on payload")
}
if payload.Status.RepositoryName != "frontend" {
t.Errorf("repository_name was dropped: got %q", payload.Status.RepositoryName)
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for git status event")
}
}