feat: sentinel-based review cleanup + system prompt file + security review
CI / test (pull_request) Successful in 14s
CI / review (gpt-4.1, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 23s
CI / review (gpt-5, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 58s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SONNET_REVIEW_TOKEN) (pull_request) Successful in 1m35s

Sentinel-based cleanup:
- Reviews embed <!-- review-bot:NAME --> in body (hidden HTML comment)
- Cleanup matches by sentinel, not token identity
- Each reviewer-name is a logical identity (sonnet, gpt, security)
- Same token can run multiple review types without conflict
- No extra API scopes needed

System prompt file (--system-prompt-file / SYSTEM_PROMPT_FILE):
- Loads a local file with additional review instructions
- Appended to system base as "Additional Review Instructions"
- Enables specialized reviews (security, performance, etc.)
- Partially addresses #5

Security review:
- SECURITY_REVIEW.md prompt focused on vulnerabilities
- 3rd CI matrix entry using same token, different prompt
- Focus: injection, auth, secrets, input validation, crypto, races

CI changes:
- REVIEWER_NAME passed from matrix.name
- SYSTEM_PROMPT_FILE passed from matrix (empty for standard reviews)
- 3 reviewers: sonnet (general), gpt (general), security (focused)
This commit is contained in:
Rodin
2026-05-01 20:55:09 -07:00
parent 41c670b44b
commit 69e0a459c3
7 changed files with 71 additions and 4 deletions
+21 -4
View File
@@ -30,6 +30,7 @@ func main() {
llmAPIKey := flag.String("llm-api-key", envOrDefault("LLM_API_KEY", ""), "LLM API key")
llmModel := flag.String("llm-model", envOrDefault("LLM_MODEL", ""), "LLM model name")
conventionsFile := flag.String("conventions-file", envOrDefault("CONVENTIONS_FILE", ""), "Conventions file path in repo (e.g. CLAUDE.md)")
systemPromptFile := flag.String("system-prompt-file", envOrDefault("SYSTEM_PROMPT_FILE", ""), "Local file with additional system prompt instructions")
patternsRepo := flag.String("patterns-repo", envOrDefault("PATTERNS_REPO", ""), "Repo with language patterns (e.g. rodin/elixir-patterns)")
patternsFiles := flag.String("patterns-files", envOrDefault("PATTERNS_FILES", "README.md"), "Comma-separated file paths to fetch from patterns repo")
dryRun := flag.Bool("dry-run", false, "Print review to stdout instead of posting")
@@ -150,9 +151,24 @@ func main() {
log.Printf("Loaded patterns from %s (%d bytes)", *patternsRepo, len(patterns))
}
// Step 6b: Load additional system prompt if specified
additionalPrompt := ""
if *systemPromptFile != "" {
data, err := os.ReadFile(*systemPromptFile)
if err != nil {
log.Fatalf("Failed to read system prompt file %q: %v", *systemPromptFile, err)
}
additionalPrompt = string(data)
log.Printf("Loaded system prompt file: %s (%d bytes)", *systemPromptFile, len(additionalPrompt))
}
// Step 7: Budget-aware prompt assembly
systemBase := review.BuildSystemBase()
if additionalPrompt != "" {
systemBase += "\n\n## Additional Review Instructions\n\n" + additionalPrompt
}
sections := budget.Sections{
SystemBase: review.BuildSystemBase(),
SystemBase: systemBase,
Patterns: patterns,
Conventions: conventions,
FileContext: fileContext,
@@ -203,14 +219,15 @@ func main() {
}
log.Printf("Review posted (id=%d, user=%s)", posted.ID, posted.User.Login)
// Delete stale reviews from this bot if update-existing is enabled
if *updateExisting && posted.User.Login != "" {
// Delete stale reviews from this bot using sentinel matching
sentinel := fmt.Sprintf("<!-- review-bot:%s -->", *reviewerName)
if *updateExisting && *reviewerName != "" {
reviews, err := giteaClient.ListReviews(ctx, owner, repoName, prNumber)
if err != nil {
log.Printf("Warning: could not list existing reviews: %v", err)
} else {
for _, r := range reviews {
if r.User.Login == posted.User.Login && r.ID != posted.ID {
if r.ID != posted.ID && strings.Contains(r.Body, sentinel) {
if err := giteaClient.DeleteReview(ctx, owner, repoName, prNumber, r.ID); err != nil {
log.Printf("Warning: could not delete old review %d: %v", r.ID, err)
} else {