-
Notifications
You must be signed in to change notification settings - Fork 1
Status and delivery follow the live overlay, not the roster primary #1064
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c1984e9
ca83f55
8cbd684
de185b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,8 @@ package main | |
| import ( | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/jim80net/flotilla/internal/roster" | ||
| ) | ||
|
|
||
| // The mirror precedence matrix: --no-mirror forces off, --mirror forces on, else the | ||
|
|
@@ -28,6 +30,32 @@ func TestShouldMirror(t *testing.T) { | |
| } | ||
| } | ||
|
|
||
| func TestResolveSendLiveDriverGenericNodeUsesOverlay(t *testing.T) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: TestResolveSendLiveDriverGenericNodeUsesOverlay duplicates the send-check block of TestGenericNodeAssessAndSendUseOverlayNotRoster in watch_test.go — same agent, overlay, cfg, inputs, and assertions. If the send-path coverage is meant to live in send_test.go, drop the duplicated send block from watch_test.go so the same scenario is asserted once. Prompt for AI agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FIX-landed at Watch coverage is
cubic-dev-ai[bot] marked this conversation as resolved.
|
||
| root := t.TempDir() | ||
| writeAgentOverlay(t, root, "backend", `{"slot":"fallback-0","surface":"codex"}`) | ||
| cfg := &roster.Config{Agents: []roster.Agent{{Name: "backend", Surface: "grok"}}} | ||
| drv, live, err := resolveSendLiveDriver(cfg, "backend", "%42", "node") | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if drv.Name() != "codex" || live != "codex" { | ||
| t.Fatalf("send driver=%q live=%q, want overlay codex (roster is grok)", drv.Name(), live) | ||
| } | ||
| } | ||
|
|
||
| func TestResolveSendLiveDriverUnregisteredOverlayFallsBackToRoster(t *testing.T) { | ||
| root := t.TempDir() | ||
| writeAgentOverlay(t, root, "backend", `{"surface":"not-a-driver"}`) | ||
| cfg := &roster.Config{Agents: []roster.Agent{{Name: "backend", Surface: "grok"}}} | ||
| drv, live, err := resolveSendLiveDriver(cfg, "backend", "%42", "node") | ||
| if err != nil { | ||
| t.Fatalf("unregistered overlay must fail-safe to roster, not error: %v", err) | ||
| } | ||
| if drv.Name() != "grok" || live != "grok" { | ||
| t.Fatalf("send driver=%q live=%q, want roster grok (overlay not-a-driver is torn)", drv.Name(), live) | ||
| } | ||
| } | ||
|
|
||
| // --mirror and --no-mirror together is a clear error (caught right after flag parse, | ||
| // before any roster load or tmux delivery). | ||
| func TestCmdSendRejectsBothMirrorFlags(t *testing.T) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ import ( | |
| "github.com/jim80net/flotilla/internal/surface" | ||
| "github.com/jim80net/flotilla/internal/utilization" | ||
| "github.com/jim80net/flotilla/internal/watch" | ||
| "github.com/jim80net/flotilla/internal/workspace" | ||
| ) | ||
|
|
||
| // cmdStatus prints a one-line-per-desk view of the fleet's last-known state. It | ||
|
|
@@ -76,7 +77,7 @@ func cmdStatus(args []string) error { | |
| if fi, statErr := os.Stat(*snapshotPath); statErr == nil { | ||
| generatedAt = fi.ModTime().UTC().Format(time.RFC3339) | ||
| } | ||
| doc := buildStatusJSON(cfg, xo, generatedAt, snap, loopByAgent, dispositions) | ||
| doc := buildStatusJSON(cfg, xo, generatedAt, snap, loopByAgent, dispositions, statusSurfaces(cfg)) | ||
| doc.Quality = harnessquality.LoadSummary(filepath.Dir(*rosterPath), now) | ||
| enc := json.NewEncoder(os.Stdout) | ||
| enc.SetIndent("", " ") | ||
|
|
@@ -124,8 +125,9 @@ type statusItem struct { | |
|
|
||
| // buildStatusJSON assembles the --json document. Pure (no I/O) so it is | ||
| // unit-testable with an in-memory snapshot; cmdStatus supplies generated_at | ||
| // (the snapshot's mtime), the loaded snapshot, and pre-derived loop evidence. | ||
| func buildStatusJSON(cfg *roster.Config, xo, generatedAt string, snap watch.Snapshot, loopByAgent map[string]loopposture.Evidence, dispositions map[string]statusSeatDisposition) statusDoc { | ||
| // (the snapshot's mtime), the loaded snapshot, pre-derived loop evidence, and | ||
| // overlay-first surfaces resolved by the command layer (statusSurfaces). | ||
| func buildStatusJSON(cfg *roster.Config, xo, generatedAt string, snap watch.Snapshot, loopByAgent map[string]loopposture.Evidence, dispositions map[string]statusSeatDisposition, surfaces map[string]string) statusDoc { | ||
| doc := statusDoc{GeneratedAt: generatedAt, XO: xo, Agents: make([]statusItem, 0, len(cfg.Agents))} | ||
| if generatedAt != "" { | ||
| doc.GeneratedAtScope = "detector_snapshot_only" | ||
|
|
@@ -146,9 +148,13 @@ func buildStatusJSON(cfg *roster.Config, xo, generatedAt string, snap watch.Snap | |
| displayPosture = "unavailable" | ||
| queueState = utilization.QueueUnknown | ||
| } | ||
| surf := a.Surface | ||
| if s, ok := surfaces[a.Name]; ok { | ||
| surf = s | ||
| } | ||
| item := statusItem{ | ||
| Name: a.Name, | ||
| Surface: effectiveSurface(a.Surface), | ||
| Surface: effectiveSurface(surf), | ||
| State: state, | ||
| LoopPosture: string(displayPosture), | ||
| QueueState: queueState, | ||
|
|
@@ -179,8 +185,27 @@ func summarizeStatusItems(items []statusItem) utilization.Summary { | |
| return utilization.Build(agents) | ||
| } | ||
|
|
||
| // effectiveSurface resolves an agent's surface name for display: an empty roster | ||
| // surface means the default driver, which the docs name "claude-code". | ||
| // statusSurfaces resolves overlay-first configured surfaces for every roster | ||
| // agent. The command layer (cmdStatus, notify) calls this so buildStatusJSON | ||
| // stays pure. | ||
| func statusSurfaces(cfg *roster.Config) map[string]string { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: statusSurfaces and loadAgentSurfaces are identical helpers (iterate roster agents, map each to EffectiveSurface) added in two different call layers, and a third copy already exists in goals.agentSurfacesFromRoster. Extract one helper into internal/workspace (e.g. EffectiveSurfaces(cfg)) and call it from status, dash server, and goals so overlay-first resolution stays in one place. Prompt for AI agents
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FIX-landed at
|
||
| return workspace.EffectiveSurfaces(namedSurfaces(cfg)) | ||
| } | ||
|
|
||
| func namedSurfaces(cfg *roster.Config) []workspace.NamedSurface { | ||
| if cfg == nil { | ||
| return nil | ||
| } | ||
| out := make([]workspace.NamedSurface, len(cfg.Agents)) | ||
| for i, a := range cfg.Agents { | ||
| out[i] = workspace.NamedSurface{Name: a.Name, RosterSurface: a.Surface} | ||
| } | ||
| return out | ||
| } | ||
|
|
||
| // effectiveSurface resolves an agent's surface name for display: empty means | ||
| // the default driver, which the docs name "claude-code". Callers pass the | ||
| // overlay-first configured surface, not the raw roster field. | ||
| func effectiveSurface(s string) string { | ||
| if s == "" { | ||
| return "claude-code" | ||
|
|
@@ -208,7 +233,7 @@ func writeStatusWithDispositions(out io.Writer, cfg *roster.Config, xo, snapshot | |
| fmt.Fprintf(out, "flotilla status — no readable detector snapshot at %s\n", snapshotPath) | ||
| fmt.Fprintln(out, " (run `flotilla watch` with change_detector: true to populate it; desks shown as unknown)") | ||
| } | ||
| utilSummary := buildStatusJSON(cfg, xo, "", snap, loopByAgent, dispositions).Utilization | ||
| utilSummary := buildStatusJSON(cfg, xo, "", snap, loopByAgent, dispositions, nil).Utilization | ||
| fmt.Fprintf(out, "Fleet — %s\n", utilization.Line(utilSummary)) | ||
| if read := utilization.WallRead(utilSummary); read != "" { | ||
| fmt.Fprintf(out, "Next — %s\n", read) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2: When
active-harness.jsoncontains a parseable but unregisteredsurfaceand the pane command is generic (node), this call makesflotilla sendfail instead of falling back to the roster. Validate the overlay surface or treat unregistered values as torn before resolving the live driver.Prompt for AI agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FIX-landed at
ca83f559b4d6a3212455f583974a04371eba9cde.workspace.EffectiveSurfacetreats an unregistered overlaySurfaceas torn (surface.Registered) and fail-safes to the roster. Coverage:TestEffectiveSurfaceUnregisteredOverlayFallsBackToRoster,TestResolveSendLiveDriverUnregisteredOverlayFallsBackToRoster(genericnode+ overlaynot-a-driver+ rostergrok→ send drivergrok, no error). Shared seam, not send-only.