feat(persona): add role-based review personas (#51)
CI / test (pull_request) Successful in 15s
CI / review (/openai/v1, gpt-4.1, gpt41, openai, GPT_REVIEW_TOKEN) (pull_request) Failing after 17s
CI / review (/anthropic/v1, claude-sonnet-4-6, sonnet, anthropic, SONNET_REVIEW_TOKEN) (pull_request) Failing after 17s
CI / review (/openai/v1, gpt-4.1-mini, gpt41-mini, openai, GPT_REVIEW_TOKEN) (pull_request) Failing after 16s
CI / review (/openai/v1, gpt-5-mini, gpt5-mini, openai, GPT_REVIEW_TOKEN) (pull_request) Failing after 14s
CI / review (/openai/v1, gpt-5, security, openai, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m28s
CI / review (/openai/v1, gpt-5, gpt, openai, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m41s

Implement role-based review personas that provide specialized review focus:
- Security: vulnerabilities, auth, secrets, injection attacks
- Architect: design patterns, code organization, API contracts
- Docs: documentation quality, API clarity, error messages

Changes:
- Add persona loading from JSON files and embedded built-ins
- Add --persona and --persona-file CLI flags (mutually exclusive)
- Add BuildPersonaSystemPrompt for persona-specific prompts
- Add FormatMarkdownWithDisplay for persona display names
- Update action.yml with persona and persona-file inputs
- Add comprehensive tests for all new functionality
- Document personas in README with examples

The persona system replaces the generic 'You are an expert code reviewer'
prompt with domain-specific identity, focus areas, ignore list, and
severity calibration. This reduces redundancy between multiple reviewers
and catches domain-specific issues that generic reviewers miss.

Closes #51
This commit is contained in:
Rodin
2026-05-10 02:49:21 -07:00
parent 2089ca0f2d
commit d7d5151a1f
13 changed files with 1261 additions and 2 deletions
+45
View File
@@ -53,3 +53,48 @@ func GiteaEvent(verdict string) string {
return "COMMENT"
}
}
// FormatMarkdownWithDisplay formats a ReviewResult with separate display name and sentinel name.
// displayName is used for the header title, sentinelName is used for the cleanup sentinel.
// If displayName is empty, sentinelName is used for both.
func FormatMarkdownWithDisplay(result *ReviewResult, displayName, sentinelName string) string {
var sb strings.Builder
// Use display name for header, or fall back to sentinel name
headerName := displayName
if headerName == "" {
headerName = sentinelName
}
if headerName != "" {
title := strings.ToUpper(headerName[:1]) + headerName[1:]
sb.WriteString(fmt.Sprintf("# %s Review\n\n", title))
}
sb.WriteString("## Summary\n\n")
sb.WriteString(result.Summary)
sb.WriteString("\n\n")
if len(result.Findings) > 0 {
sb.WriteString("## Findings\n\n")
sb.WriteString("| # | Severity | File | Line | Finding |\n")
sb.WriteString("|---|----------|------|------|--------|\n")
for i, f := range result.Findings {
sb.WriteString(fmt.Sprintf("| %d | [%s] | `%s` | %d | %s |\n",
i+1, f.Severity, f.File, f.Line, f.Finding))
}
sb.WriteString("\n")
}
sb.WriteString("## Recommendation\n\n")
sb.WriteString(fmt.Sprintf("**%s** — %s\n", result.Verdict, result.Recommendation))
if sentinelName != "" {
sb.WriteString(fmt.Sprintf("\n---\n*Review by %s*\n", headerName))
// Hidden sentinel for identifying this bot's reviews during cleanup
sb.WriteString(fmt.Sprintf("\n<!-- review-bot:%s -->\n", sentinelName))
}
return sb.String()
}