// Package prompt builds the task-specific instructions handed to a one-shot // Claude Code session for each kind of Gitea event. package prompt import ( "fmt" "strings" "git.unkin.net/unkin/teabot/internal/gitea" ) // conventions is appended to every prompt so sessions follow the house rules // (branch naming, PR body shape, and the loop-safety expectation of finishing // in one pass). const conventions = `Conventions you MUST follow: - Work on a branch named benvin/; never push to the default branch. - Use HTTPS remotes for git.unkin.net (SSH is blocked). The clone is already authenticated. - PR descriptions have a short "why" paragraph followed by present-tense "how" bullets. - Use the tea CLI (already configured for your bot identity) for Gitea actions such as creating PRs and posting comments; never use gh. - This is a single non-interactive session: complete the task in one pass, then stop. Do not wait for input.` // IssueContext carries everything needed to prompt an implementer session for a // newly opened issue. type IssueContext struct { Repo string PersonalityName string Issue gitea.Issue Comments []gitea.Comment } // Issue builds the prompt for reviewing and possibly implementing an issue. func Issue(c IssueContext) string { var b strings.Builder fmt.Fprintf(&b, "You are %q, an autonomous implementer bot acting on the Gitea repository %s.\n\n", c.PersonalityName, c.Repo) fmt.Fprintf(&b, "A new issue was opened. Review it and decide whether it warrants a code change.\n") fmt.Fprintf(&b, "If it does, implement the change on a new branch and open a pull request that links the issue with \"Closes #%d\".\n", c.Issue.Index) fmt.Fprintf(&b, "If it does NOT warrant a code change (question, discussion, invalid, needs clarification), post a brief comment on the issue explaining your assessment instead of opening a PR.\n\n") fmt.Fprintf(&b, "Issue #%d: %s\n", c.Issue.Index, c.Issue.Title) fmt.Fprintf(&b, "Opened by: %s\n", c.Issue.Poster.Login) fmt.Fprintf(&b, "URL: %s\n\n", c.Issue.HTMLURL) b.WriteString("Issue body:\n") b.WriteString(bodyOrNone(c.Issue.Body)) b.WriteString("\n") writeComments(&b, c.Comments) b.WriteString("\n") b.WriteString(conventions) return b.String() } // PullContext carries everything needed to prompt a reviewer session for a PR. type PullContext struct { Repo string PersonalityName string Pull gitea.PullRequest Diff string Comments []gitea.Comment } // Pull builds the prompt for reviewing a pull request. func Pull(c PullContext) string { var b strings.Builder fmt.Fprintf(&b, "You are %q, an autonomous reviewer bot acting on the Gitea repository %s.\n\n", c.PersonalityName, c.Repo) fmt.Fprintf(&b, "A new pull request was opened. Review it thoroughly and decide whether the change is correct and ready.\n") b.WriteString("Judge: does the diff actually satisfy the PR description? Is it correct, tested, and consistent with the codebase's style?\n") fmt.Fprintf(&b, "Post your review on PR #%d using tea: an approving review if it is good, or a review requesting changes with specific, actionable comments if not.\n\n", c.Pull.Index) fmt.Fprintf(&b, "PR #%d: %s\n", c.Pull.Index, c.Pull.Title) fmt.Fprintf(&b, "Opened by: %s\n", c.Pull.Poster.Login) fmt.Fprintf(&b, "Branch: %s -> %s\n", c.Pull.Head.Ref, c.Pull.Base.Ref) fmt.Fprintf(&b, "URL: %s\n\n", c.Pull.HTMLURL) b.WriteString("PR description:\n") b.WriteString(bodyOrNone(c.Pull.Body)) b.WriteString("\n") writeComments(&b, c.Comments) if strings.TrimSpace(c.Diff) != "" { b.WriteString("\nUnified diff:\n") b.WriteString(truncate(c.Diff, 60000)) b.WriteString("\n") } b.WriteString("\n") b.WriteString(conventions) return b.String() } // FollowUpKind distinguishes a follow-up on an issue vs a pull request. type FollowUpKind string const ( // FollowUpIssue is a new comment on an issue teabot acted on. FollowUpIssue FollowUpKind = "issue" // FollowUpPull is a new comment on a pull request teabot acted on. FollowUpPull FollowUpKind = "pull" ) // FollowUpContext carries everything needed to prompt a follow-up session for a // new comment on a thread teabot previously engaged with. type FollowUpContext struct { Repo string PersonalityName string Kind FollowUpKind Index int64 Title string URL string Comments []gitea.Comment // full thread, oldest first NewComment gitea.Comment // the comment that triggered this follow-up } // FollowUp builds the prompt for responding to a new comment. func FollowUp(c FollowUpContext) string { var b strings.Builder noun := "issue" if c.Kind == FollowUpPull { noun = "pull request" } fmt.Fprintf(&b, "You are %q, an autonomous bot acting on the Gitea repository %s.\n\n", c.PersonalityName, c.Repo) fmt.Fprintf(&b, "A new comment was posted on %s #%d, a thread you previously worked on. Read the full thread and respond or take action as appropriate.\n", noun, c.Index) if c.Kind == FollowUpPull { b.WriteString("If the comment requests changes, check out the PR branch, make the changes, and push them; then reply summarising what you did.\n") } else { b.WriteString("If the comment asks for a change or clarifies the request, implement it on a branch and open or update the PR; otherwise reply with a helpful comment.\n") } fmt.Fprintf(&b, "\n%s #%d: %s\n", strings.ToUpper(noun[:1])+noun[1:], c.Index, c.Title) fmt.Fprintf(&b, "URL: %s\n\n", c.URL) b.WriteString("Full thread (oldest first):\n") writeComments(&b, c.Comments) fmt.Fprintf(&b, "\nThe new comment that triggered this task was posted by %s:\n", c.NewComment.Poster.Login) b.WriteString(bodyOrNone(c.NewComment.Body)) b.WriteString("\n\n") b.WriteString(conventions) return b.String() } func writeComments(b *strings.Builder, comments []gitea.Comment) { if len(comments) == 0 { return } b.WriteString("\nComments (oldest first):\n") for _, c := range comments { fmt.Fprintf(b, "- %s: %s\n", c.Poster.Login, oneLine(c.Body)) } } func bodyOrNone(s string) string { if strings.TrimSpace(s) == "" { return "(no description provided)" } return s } func oneLine(s string) string { s = strings.ReplaceAll(s, "\r\n", "\n") s = strings.ReplaceAll(s, "\n", " ") return strings.TrimSpace(s) } func truncate(s string, max int) string { if len(s) <= max { return s } return s[:max] + "\n... [diff truncated] ..." }