diff --git a/server/internal/handler/squad_briefing.go b/server/internal/handler/squad_briefing.go index e02a8a777..cc55baf1d 100644 --- a/server/internal/handler/squad_briefing.go +++ b/server/internal/handler/squad_briefing.go @@ -169,13 +169,13 @@ func buildSquadExecutionState(ctx context.Context, q *db.Queries, issueID pgtype sb.WriteString("This is a live snapshot taken when you were triggered — read it before you decide.\n\n") if len(workers) == 0 { - sb.WriteString("**No worker session is currently running on this issue.** ") - sb.WriteString("No delegated agent is executing right now. A worker's task ends the moment it stops producing output, so a recent comment that reads like work is still in progress (\"I'll continue later\", \"still verifying\", a partial/interim report) does NOT mean anything is running — nothing resumes automatically. ") + sb.WriteString("**No worker task is currently active on this issue.** ") + sb.WriteString("No delegated agent has an active queued, waiting, or running task right now. A worker's task ends the moment it stops producing output, so a recent comment that reads like work is still in progress (\"I'll continue later\", \"still verifying\", a partial/interim report) does NOT mean anything is active — nothing resumes automatically. ") sb.WriteString("Before you record `no_action`, confirm the issue is genuinely done or genuinely waiting on a human. If work remains, delegate the next step, or mark the issue `blocked` and escalate. An interim report with no active session is a stalled issue, not a progressing one.\n") return sb.String() } - fmt.Fprintf(&sb, "**%d worker session(s) currently running on this issue.** A delegated agent is executing right now:\n", len(workers)) + fmt.Fprintf(&sb, "**%d worker task(s) currently active on this issue.** A delegated agent has queued, waiting, or running work:\n", len(workers)) for _, t := range workers { name := "a squad member" if ag, err := q.GetAgent(ctx, t.AgentID); err == nil { @@ -183,7 +183,7 @@ func buildSquadExecutionState(ctx context.Context, q *db.Queries, issueID pgtype } fmt.Fprintf(&sb, "- %s — status `%s`\n", name, t.Status) } - sb.WriteString("\nYou usually do NOT need to delegate the same work again while a session is in flight — recording `no_action` and waiting for it to finish is appropriate. Re-delegate only if the running work is clearly wrong or stuck, or hand off a genuinely independent next step.\n") + sb.WriteString("\nYou usually do NOT need to delegate the same work again while a worker task is in flight — recording `no_action` and waiting for it to finish is appropriate. Re-delegate only if the active work is clearly wrong or stuck, or hand off a genuinely independent next step.\n") return sb.String() } diff --git a/server/internal/handler/squad_briefing_test.go b/server/internal/handler/squad_briefing_test.go index e64370aad..e4799e08c 100644 --- a/server/internal/handler/squad_briefing_test.go +++ b/server/internal/handler/squad_briefing_test.go @@ -226,34 +226,34 @@ func TestBuildSquadLeaderBriefing_MentionsRoundTrip(t *testing.T) { // claimAndDecodeAgent runs ClaimTaskByRuntime for the given runtime and // returns the agent block of the response. Fails the test on non-200. func claimAndDecodeAgent(t *testing.T, runtimeID string) *TaskAgentData { -t.Helper() -w := httptest.NewRecorder() -req := newDaemonTokenRequest("POST", "/api/daemon/runtimes/"+runtimeID+"/claim", nil, testWorkspaceID, "test-claim-squad-briefing") -req = withURLParam(req, "runtimeId", runtimeID) -testHandler.ClaimTaskByRuntime(w, req) -if w.Code != http.StatusOK { -t.Fatalf("ClaimTaskByRuntime: %d %s", w.Code, w.Body.String()) -} -var resp struct { -Task *struct { -Agent *TaskAgentData `json:"agent"` -} `json:"task"` -} -if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { -t.Fatalf("decode: %v", err) -} -if resp.Task == nil || resp.Task.Agent == nil { -t.Fatalf("expected task.agent in response, got: %s", w.Body.String()) -} -return resp.Task.Agent + t.Helper() + w := httptest.NewRecorder() + req := newDaemonTokenRequest("POST", "/api/daemon/runtimes/"+runtimeID+"/claim", nil, testWorkspaceID, "test-claim-squad-briefing") + req = withURLParam(req, "runtimeId", runtimeID) + testHandler.ClaimTaskByRuntime(w, req) + if w.Code != http.StatusOK { + t.Fatalf("ClaimTaskByRuntime: %d %s", w.Code, w.Body.String()) + } + var resp struct { + Task *struct { + Agent *TaskAgentData `json:"agent"` + } `json:"task"` + } + if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { + t.Fatalf("decode: %v", err) + } + if resp.Task == nil || resp.Task.Agent == nil { + t.Fatalf("expected task.agent in response, got: %s", w.Body.String()) + } + return resp.Task.Agent } // queueSquadIssueTaskFor creates an issue assigned to the squad and a queued // task for the given (agentID, runtimeID). Returns the issue + task IDs. func queueSquadIssueTaskFor(t *testing.T, squadID, agentID, runtimeID string, issueNumber int) (issueID, taskID string) { -t.Helper() -ctx := context.Background() -if err := testPool.QueryRow(ctx, ` + t.Helper() + ctx := context.Background() + if err := testPool.QueryRow(ctx, ` INSERT INTO issue ( workspace_id, title, status, priority, creator_id, creator_type, assignee_type, assignee_id, number, position @@ -261,101 +261,101 @@ assignee_type, assignee_id, number, position 'squad', $3, $4, 0) RETURNING id `, testWorkspaceID, testUserID, squadID, issueNumber).Scan(&issueID); err != nil { -t.Fatalf("create squad-assigned issue: %v", err) -} -t.Cleanup(func() { testPool.Exec(ctx, `DELETE FROM issue WHERE id = $1`, issueID) }) + t.Fatalf("create squad-assigned issue: %v", err) + } + t.Cleanup(func() { testPool.Exec(ctx, `DELETE FROM issue WHERE id = $1`, issueID) }) -if err := testPool.QueryRow(ctx, ` + if err := testPool.QueryRow(ctx, ` INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority) VALUES ($1, $2, $3, 'queued', 0) RETURNING id `, agentID, runtimeID, issueID).Scan(&taskID); err != nil { -t.Fatalf("queue task: %v", err) -} -t.Cleanup(func() { testPool.Exec(ctx, `DELETE FROM agent_task_queue WHERE id = $1`, taskID) }) -return + t.Fatalf("queue task: %v", err) + } + t.Cleanup(func() { testPool.Exec(ctx, `DELETE FROM agent_task_queue WHERE id = $1`, taskID) }) + return } // TestClaimTask_LeaderGetsBriefing — when the squad leader claims a task on // a squad-assigned issue, the response's agent.instructions must include // the Operating Protocol + Roster + user instructions. func TestClaimTask_LeaderGetsBriefing(t *testing.T) { -if testHandler == nil { -t.Skip("database not available") -} -ctx := context.Background() + if testHandler == nil { + t.Skip("database not available") + } + ctx := context.Background() -var leaderID, runtimeID string -if err := testPool.QueryRow(ctx, -`SELECT id, runtime_id FROM agent WHERE workspace_id = $1 ORDER BY created_at ASC LIMIT 1`, -testWorkspaceID, -).Scan(&leaderID, &runtimeID); err != nil { -t.Fatalf("get leader agent: %v", err) -} + var leaderID, runtimeID string + if err := testPool.QueryRow(ctx, + `SELECT id, runtime_id FROM agent WHERE workspace_id = $1 ORDER BY created_at ASC LIMIT 1`, + testWorkspaceID, + ).Scan(&leaderID, &runtimeID); err != nil { + t.Fatalf("get leader agent: %v", err) + } -squad := seedSquadForBriefing(t, leaderID, "Briefing Claim Squad", "Be terse.") + squad := seedSquadForBriefing(t, leaderID, "Briefing Claim Squad", "Be terse.") -helper := createHandlerTestAgent(t, "Briefing Helper", []byte("[]")) -addAgentMember(t, squad.ID, helper, "implementer") + helper := createHandlerTestAgent(t, "Briefing Helper", []byte("[]")) + addAgentMember(t, squad.ID, helper, "implementer") -queueSquadIssueTaskFor(t, util.UUIDToString(squad.ID), leaderID, runtimeID, 95001) + queueSquadIssueTaskFor(t, util.UUIDToString(squad.ID), leaderID, runtimeID, 95001) -agent := claimAndDecodeAgent(t, runtimeID) -for _, want := range []string{ -"## Squad Operating Protocol", -"## Squad Roster", -"Leader (you):", -"## Squad Instructions (Briefing Claim Squad)", -"Be terse.", -"`[@Briefing Helper](mention://agent/" + helper + ")`", -} { -if !strings.Contains(agent.Instructions, want) { -t.Errorf("expected agent.instructions to contain %q\n--- instructions ---\n%s", want, agent.Instructions) -} -} + agent := claimAndDecodeAgent(t, runtimeID) + for _, want := range []string{ + "## Squad Operating Protocol", + "## Squad Roster", + "Leader (you):", + "## Squad Instructions (Briefing Claim Squad)", + "Be terse.", + "`[@Briefing Helper](mention://agent/" + helper + ")`", + } { + if !strings.Contains(agent.Instructions, want) { + t.Errorf("expected agent.instructions to contain %q\n--- instructions ---\n%s", want, agent.Instructions) + } + } } // TestClaimTask_NonLeaderGetsNoBriefing — when a non-leader squad member // claims a task on a squad-assigned issue, NO briefing is injected. func TestClaimTask_NonLeaderGetsNoBriefing(t *testing.T) { -if testHandler == nil { -t.Skip("database not available") -} -ctx := context.Background() + if testHandler == nil { + t.Skip("database not available") + } + ctx := context.Background() -var leaderID string -if err := testPool.QueryRow(ctx, -`SELECT id FROM agent WHERE workspace_id = $1 ORDER BY created_at ASC LIMIT 1`, -testWorkspaceID, -).Scan(&leaderID); err != nil { -t.Fatalf("get leader agent: %v", err) -} + var leaderID string + if err := testPool.QueryRow(ctx, + `SELECT id FROM agent WHERE workspace_id = $1 ORDER BY created_at ASC LIMIT 1`, + testWorkspaceID, + ).Scan(&leaderID); err != nil { + t.Fatalf("get leader agent: %v", err) + } -squad := seedSquadForBriefing(t, leaderID, "Non-Leader Squad", "Squad guidance.") + squad := seedSquadForBriefing(t, leaderID, "Non-Leader Squad", "Squad guidance.") -// Create a second agent (NOT the leader) with its own runtime so the -// claim path picks its task without ambiguity. -helperID := createHandlerTestAgent(t, "Non Leader Helper", []byte("[]")) -addAgentMember(t, squad.ID, helperID, "") -var helperRuntime string -if err := testPool.QueryRow(ctx, -`SELECT runtime_id FROM agent WHERE id = $1`, helperID, -).Scan(&helperRuntime); err != nil { -t.Fatalf("get helper runtime: %v", err) -} + // Create a second agent (NOT the leader) with its own runtime so the + // claim path picks its task without ambiguity. + helperID := createHandlerTestAgent(t, "Non Leader Helper", []byte("[]")) + addAgentMember(t, squad.ID, helperID, "") + var helperRuntime string + if err := testPool.QueryRow(ctx, + `SELECT runtime_id FROM agent WHERE id = $1`, helperID, + ).Scan(&helperRuntime); err != nil { + t.Fatalf("get helper runtime: %v", err) + } -queueSquadIssueTaskFor(t, util.UUIDToString(squad.ID), helperID, helperRuntime, 95002) + queueSquadIssueTaskFor(t, util.UUIDToString(squad.ID), helperID, helperRuntime, 95002) -agent := claimAndDecodeAgent(t, helperRuntime) -for _, mustNot := range []string{ -"Squad Operating Protocol", -"Squad Roster", -"Squad Instructions (Non-Leader Squad)", -} { -if strings.Contains(agent.Instructions, mustNot) { -t.Errorf("non-leader claim should NOT contain %q\n--- instructions ---\n%s", mustNot, agent.Instructions) -} -} + agent := claimAndDecodeAgent(t, helperRuntime) + for _, mustNot := range []string{ + "Squad Operating Protocol", + "Squad Roster", + "Squad Instructions (Non-Leader Squad)", + } { + if strings.Contains(agent.Instructions, mustNot) { + t.Errorf("non-leader claim should NOT contain %q\n--- instructions ---\n%s", mustNot, agent.Instructions) + } + } } // seedSquadIssue creates a squad-assigned issue and returns its UUID. @@ -415,14 +415,14 @@ func TestBuildSquadLeaderBriefing_ExecutionState(t *testing.T) { if !strings.Contains(out, "## Current Execution State") { t.Fatalf("expected Current Execution State section, got:\n%s", out) } - if !strings.Contains(out, "No worker session is currently running on this issue.") { + if !strings.Contains(out, "No worker task is currently active on this issue.") { t.Errorf("expected no-worker-session line, got:\n%s", out) } // The leader's own active task must NOT count as a worker session. insertActiveTask(t, leaderID, issueID, true) out = buildSquadLeaderBriefing(ctx, testHandler.Queries, squad, issueID) - if !strings.Contains(out, "No worker session is currently running on this issue.") { + if !strings.Contains(out, "No worker task is currently active on this issue.") { t.Errorf("leader's own task must not count as a worker session, got:\n%s", out) } @@ -431,7 +431,7 @@ func TestBuildSquadLeaderBriefing_ExecutionState(t *testing.T) { insertActiveTask(t, worker, issueID, false) out = buildSquadLeaderBriefing(ctx, testHandler.Queries, squad, issueID) for _, want := range []string{ - "worker session(s) currently running on this issue.", + "worker task(s) currently active on this issue.", "Exec Worker", "status `running`", } {