From fdf19cac8f3ff3a159aa8bcc9aaaf65327a1accb Mon Sep 17 00:00:00 2001 From: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com> Date: Thu, 14 May 2026 17:48:02 +0800 Subject: [PATCH] fix(quick-create): default squad-picked issues to the squad, not the leader (#2611) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the user opens quick-create with a squad selected, the task is enqueued against the squad's leader agent — but the squad, not the leader, is the expected owner. The prompt previously instructed the leader to "default to YOURSELF" using its own agent UUID, hiding new issues from the squad's delegation flow. Surface the squad's id + name on the claim response and branch the default-assignee instruction in buildQuickCreatePrompt: when SquadID is present, point --assignee-id at the squad UUID and explicitly forbid self-assignment. MUL-2203 Co-authored-by: multica-agent --- server/internal/daemon/prompt.go | 17 ++++++++-- server/internal/daemon/prompt_test.go | 46 +++++++++++++++++++++++++++ server/internal/daemon/types.go | 2 ++ server/internal/handler/agent.go | 2 ++ server/internal/handler/daemon.go | 5 +++ 5 files changed, 69 insertions(+), 3 deletions(-) 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,