diff --git a/server/internal/daemon/prompt.go b/server/internal/daemon/prompt.go index 3b439988d..512e1755b 100644 --- a/server/internal/daemon/prompt.go +++ b/server/internal/daemon/prompt.go @@ -73,11 +73,22 @@ func buildQuickCreatePrompt(task Task) string { agentID = task.Agent.ID agentName = task.Agent.Name } - if agentID != "" { + switch { + case task.SquadID != "": + // The user opened quick-create with a SQUAD selected. The task + // runs on the squad's leader agent, but the squad is the expected + // owner — assigning to the leader would mask the squad's + // delegation flow. Always point the default at the squad UUID. + if task.SquadName != "" { + fmt.Fprintf(&b, " - When the user did NOT name an assignee, default to the picker SQUAD %q: pass `--assignee-id %q` (the squad's UUID). The user opened quick-create with the squad selected; you (the leader agent) are running on the squad's behalf, so the squad — not you — is the expected owner. Never leave the issue unassigned, and do not assign it to your own agent UUID.\n\n", task.SquadName, task.SquadID) + } else { + fmt.Fprintf(&b, " - When the user did NOT name an assignee, default to the picker SQUAD: pass `--assignee-id %q` (the squad's UUID). The user opened quick-create with the squad selected; you (the leader agent) are running on the squad's behalf, so the squad — not you — is the expected owner. Never leave the issue unassigned, and do not assign it to your own agent UUID.\n\n", task.SquadID) + } + case agentID != "": fmt.Fprintf(&b, " - When the user did NOT name an assignee, default to YOURSELF: pass `--assignee-id %q` (your agent UUID). The picker agent is the expected owner because the user opened quick-create with you selected — never leave the issue unassigned. Use the UUID flag, not `--assignee `, so the assignment is unambiguous even when other agents share part of your name.\n\n", agentID) - } else if agentName != "" { + case agentName != "": fmt.Fprintf(&b, " - When the user did NOT name an assignee, default to YOURSELF: pass `--assignee %q`. The picker agent is the expected owner because the user opened quick-create with you selected — never leave the issue unassigned.\n\n", agentName) - } else { + default: b.WriteString(" - When the user did NOT name an assignee, default to YOURSELF (the picker agent): pass `--assignee-id ` (preferred) or `--assignee `. Never leave the issue unassigned.\n\n") } diff --git a/server/internal/daemon/prompt_test.go b/server/internal/daemon/prompt_test.go index 2ecf5d50d..b58d709c6 100644 --- a/server/internal/daemon/prompt_test.go +++ b/server/internal/daemon/prompt_test.go @@ -62,6 +62,52 @@ func TestBuildQuickCreatePromptAssigneeIncludesSquads(t *testing.T) { } } +// TestBuildQuickCreatePromptSquadDefaultsToSquad locks in the MUL-2203 +// fix: when the picker was a squad, the task runs on the squad's leader +// agent, but the default assignee for issues created by this run must +// point at the SQUAD's UUID — not the leader agent's UUID. The previous +// "default to YOURSELF" instruction made squad-created issues land under +// the leader, hiding them from the squad's delegation flow. +func TestBuildQuickCreatePromptSquadDefaultsToSquad(t *testing.T) { + const ( + squadID = "aaaa1111-2222-3333-4444-555555555555" + squadName = "独立团" + leaderID = "bbbb1111-2222-3333-4444-666666666666" + ) + out := buildQuickCreatePrompt(Task{ + QuickCreatePrompt: "fix the login button color", + Agent: &AgentData{ID: leaderID, Name: "leader-agent"}, + SquadID: squadID, + SquadName: squadName, + }) + + // The default-assignee instruction must point at the squad UUID. + if !strings.Contains(out, "--assignee-id \""+squadID+"\"") { + t.Errorf("buildQuickCreatePrompt with SquadID must default to the squad's UUID, got:\n%s", out) + } + // And it must NOT tell the agent to default to itself (the leader). + if strings.Contains(out, "--assignee-id \""+leaderID+"\"") { + t.Errorf("buildQuickCreatePrompt with SquadID must NOT default to the leader agent's UUID, got:\n%s", out) + } + // The squad name should appear in the instruction so the agent has + // human-readable context for the routing decision. + if !strings.Contains(out, squadName) { + t.Errorf("buildQuickCreatePrompt with SquadID should mention the squad name %q, got:\n%s", squadName, out) + } + // And the prompt must explicitly call out the squad-vs-leader rule + // so the agent does not silently regress to "default to YOURSELF". + mustContain := []string{ + "picker SQUAD", + "running on the squad's behalf", + "do not assign it to your own agent UUID", + } + for _, s := range mustContain { + if !strings.Contains(out, s) { + t.Errorf("buildQuickCreatePrompt with SquadID missing %q\n--- output ---\n%s", s, out) + } + } +} + // TestBuildQuickCreatePromptProjectPinning verifies that when the user // pins a project in the quick-create modal, the prompt instructs the agent // to pass `--project ` exactly. Without this, the agent would re-read diff --git a/server/internal/daemon/types.go b/server/internal/daemon/types.go index c8bde827e..da0778b42 100644 --- a/server/internal/daemon/types.go +++ b/server/internal/daemon/types.go @@ -59,6 +59,8 @@ type Task struct { AutopilotSource string `json:"autopilot_source,omitempty"` // manual, schedule, webhook, or api AutopilotTriggerPayload json.RawMessage `json:"autopilot_trigger_payload,omitempty"` // optional trigger payload for webhook/api runs QuickCreatePrompt string `json:"quick_create_prompt,omitempty"` // user's natural-language input for quick-create tasks + SquadID string `json:"squad_id,omitempty"` // when the picker was a squad, the squad's UUID; Agent is still the resolved leader + SquadName string `json:"squad_name,omitempty"` // display name for the picker squad, used in prompt text } // ChatAttachmentMeta is the structured attachment metadata the daemon diff --git a/server/internal/handler/agent.go b/server/internal/handler/agent.go index 257a94ba2..5b265292e 100644 --- a/server/internal/handler/agent.go +++ b/server/internal/handler/agent.go @@ -174,6 +174,8 @@ type AgentTaskResponse struct { AutopilotSource string `json:"autopilot_source,omitempty"` // manual, schedule, webhook, or api AutopilotTriggerPayload json.RawMessage `json:"autopilot_trigger_payload,omitempty"` // optional trigger payload for webhook/api runs QuickCreatePrompt string `json:"quick_create_prompt,omitempty"` // user's natural-language input for quick-create tasks + SquadID string `json:"squad_id,omitempty"` // for quick-create tasks where the picker was a squad; Agent is still the resolved leader + SquadName string `json:"squad_name,omitempty"` // display name for the picker squad Kind string `json:"kind"` // discriminator: "comment" | "autopilot" | "chat" | "quick_create" | "direct" — used by the activity row to label tasks that have no linked issue } diff --git a/server/internal/handler/daemon.go b/server/internal/handler/daemon.go index 2f5f2b7fa..05f3f9697 100644 --- a/server/internal/handler/daemon.go +++ b/server/internal/handler/daemon.go @@ -1416,6 +1416,11 @@ func (h *Handler) ClaimTaskByRuntime(w http.ResponseWriter, r *http.Request) { } else { resp.Agent.Instructions = resp.Agent.Instructions + "\n\n" + briefing } + // Surface the squad identity to the daemon so the + // quick-create prompt defaults the new issue's + // assignee to the squad, not the leader agent. + resp.SquadID = uuidToString(squad.ID) + resp.SquadName = squad.Name slog.Debug("injected squad leader briefing for quick-create", "squad_id", uuidToString(squad.ID), "squad_name", squad.Name,