package daemon import ( "fmt" "strings" "github.com/multica-ai/multica/server/internal/daemon/execenv" ) // BuildPrompt constructs the task prompt for an agent CLI. // Keep this minimal — detailed instructions live in CLAUDE.md / AGENTS.md // injected by execenv.InjectRuntimeConfig. func BuildPrompt(task Task) string { if task.ChatSessionID != "" { return buildChatPrompt(task) } if task.TriggerCommentID != "" { return buildCommentPrompt(task) } if task.AutopilotRunID != "" { return buildAutopilotPrompt(task) } if task.QuickCreatePrompt != "" { return buildQuickCreatePrompt(task) } var b strings.Builder b.WriteString("You are running as a local coding agent for a Multica workspace.\n\n") fmt.Fprintf(&b, "Your assigned issue ID is: %s\n\n", task.IssueID) fmt.Fprintf(&b, "Start by running `multica issue get %s --output json` to understand your task, then complete it.\n", task.IssueID) return b.String() } // buildQuickCreatePrompt constructs a prompt for quick-create tasks. The // user typed a single natural-language sentence in the create-issue modal; // the agent's only job is to translate it into one `multica issue create` // CLI invocation. No issue exists yet, so the agent must NOT call // `multica issue get` or attempt to comment — there's nothing to read or // reply to. func buildQuickCreatePrompt(task Task) string { var b strings.Builder b.WriteString("You are running as a quick-create assistant for a Multica workspace.\n\n") b.WriteString("A user pressed the quick-create shortcut and typed a one-line description. There is NO existing issue. Your only job is to translate the description into a single `multica issue create` command and run it.\n\n") fmt.Fprintf(&b, "User input:\n> %s\n\n", task.QuickCreatePrompt) b.WriteString("Field rules:\n") b.WriteString("- title: required. A short, imperative summary extracted from the user input (e.g. \"fix inbox loading\"). Strip filler words.\n") b.WriteString("- description: optional. Include only if the user supplied detail beyond the title; otherwise omit. Never echo the title here.\n") b.WriteString("- priority: one of `urgent`, `high`, `medium`, `low`, or omit. Map P0/P1 → urgent/high; \"asap\"/\"紧急\" → urgent; \"低优先级\" → low. If unspecified, omit.\n") b.WriteString("- assignee:\n") b.WriteString(" - When the user names someone (\"分给 X\" / \"assign to X\" / \"@X\"), call `multica workspace members --output json` and find the matching member by display name (case-insensitive substring match is fine). On a clean match, pass `--assignee `. On no match or ambiguous match, do NOT pass `--assignee` — instead append a final line to the description: `未识别 assignee: X`.\n") agentName := "" if task.Agent != nil { agentName = task.Agent.Name } if 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", agentName) } else { b.WriteString(" - When the user did NOT name an assignee, default to YOURSELF (the picker agent): pass `--assignee `. Never leave the issue unassigned.\n") } b.WriteString("- project: omit. The platform will route the issue to the workspace default.\n") b.WriteString("- status: omit (defaults to `todo`).\n") b.WriteString("- attachments: do NOT pass `--attachment`. The flag only accepts LOCAL file paths, and any image URL embedded in the user input is already part of the description as markdown — keep it inline in `--description` instead of trying to re-attach it. (Trying to pass `https://…` to `--attachment` will fail and look like a create error to you, but the issue may already exist; never retry `issue create` on that signal.)\n\n") b.WriteString("Output format:\n") b.WriteString("- Run exactly one `multica issue create` invocation. Do not retry it for any reason — even on a non-zero exit. The issue may already exist; another attempt would create a duplicate.\n") b.WriteString("- After it succeeds, print exactly one line: `Created MUL-: ` and exit. No commentary, no follow-up tool calls.\n") b.WriteString("- Do NOT call `multica issue get` or `multica issue comment add` for this task — there is no issue to query or comment on prior to creation.\n") b.WriteString("- If the CLI returns an error, exit with that error as the only output. The platform writes a failure notification automatically; do not retry.\n") return b.String() } // buildCommentPrompt constructs a prompt for comment-triggered tasks. // The triggering comment content is embedded directly so the agent cannot // miss it, even when stale output files exist in a reused workdir. // The reply instructions (including the current TriggerCommentID as --parent) // are re-emitted on every turn so resumed sessions cannot carry forward a // previous turn's --parent UUID. func buildCommentPrompt(task Task) string { var b strings.Builder b.WriteString("You are running as a local coding agent for a Multica workspace.\n\n") fmt.Fprintf(&b, "Your assigned issue ID is: %s\n\n", task.IssueID) if task.TriggerCommentContent != "" { authorLabel := "A user" if task.TriggerAuthorType == "agent" { name := task.TriggerAuthorName if name == "" { name = "another agent" } authorLabel = fmt.Sprintf("Another agent (%s)", name) } fmt.Fprintf(&b, "[NEW COMMENT] %s just left a new comment. Focus on THIS comment — do not confuse it with previous ones:\n\n", authorLabel) fmt.Fprintf(&b, "> %s\n\n", task.TriggerCommentContent) if task.TriggerAuthorType == "agent" { b.WriteString("⚠️ The triggering comment was posted by another agent. Decide whether a reply is warranted. If you produced actual work this turn (investigated, fixed something, answered a real question), post the result as a normal reply — that is NOT a noise comment, and the standard rule that final results must be delivered via comment still applies. If the triggering comment was a pure acknowledgment, thanks, or sign-off AND you produced no work this turn, do NOT reply — and do NOT post a comment saying 'No reply needed' or similar. Simply exit with no output. Silence is the preferred way to end agent-to-agent threads. If you do reply, do not @mention the other agent as a sign-off (that re-triggers them and starts a loop).\n\n") } } fmt.Fprintf(&b, "Start by running `multica issue get %s --output json` to understand your task, then decide how to proceed.\n\n", task.IssueID) b.WriteString(execenv.BuildCommentReplyInstructions(task.IssueID, task.TriggerCommentID)) return b.String() } // buildChatPrompt constructs a prompt for interactive chat tasks. func buildChatPrompt(task Task) string { var b strings.Builder b.WriteString("You are running as a chat assistant for a Multica workspace.\n") b.WriteString("A user is chatting with you directly. Respond to their message.\n\n") fmt.Fprintf(&b, "User message:\n%s\n", task.ChatMessage) return b.String() } // buildAutopilotPrompt constructs a prompt for run_only autopilot tasks. func buildAutopilotPrompt(task Task) string { var b strings.Builder b.WriteString("You are running as a local coding agent for a Multica workspace.\n\n") b.WriteString("This task was triggered by an Autopilot in run-only mode. There is no assigned Multica issue for this run.\n\n") fmt.Fprintf(&b, "Autopilot run ID: %s\n", task.AutopilotRunID) if task.AutopilotID != "" { fmt.Fprintf(&b, "Autopilot ID: %s\n", task.AutopilotID) } if task.AutopilotTitle != "" { fmt.Fprintf(&b, "Autopilot title: %s\n", task.AutopilotTitle) } if task.AutopilotSource != "" { fmt.Fprintf(&b, "Trigger source: %s\n", task.AutopilotSource) } if strings.TrimSpace(string(task.AutopilotTriggerPayload)) != "" { fmt.Fprintf(&b, "Trigger payload:\n%s\n", strings.TrimSpace(string(task.AutopilotTriggerPayload))) } b.WriteString("\nAutopilot instructions:\n") if strings.TrimSpace(task.AutopilotDescription) != "" { b.WriteString(task.AutopilotDescription) b.WriteString("\n\n") } else if task.AutopilotTitle != "" { fmt.Fprintf(&b, "%s\n\n", task.AutopilotTitle) } else { b.WriteString("No additional autopilot instructions were provided. Inspect the autopilot configuration before proceeding.\n\n") } if task.AutopilotID != "" { fmt.Fprintf(&b, "Start by running `multica autopilot get %s --output json` if you need the full autopilot configuration, then complete the instructions above.\n", task.AutopilotID) } else { b.WriteString("Complete the instructions above.\n") } b.WriteString("Do not run `multica issue get`; this run does not have an issue ID.\n") return b.String() }