worker: add reviewer stage (branch→review→push) with R1-R6 errors
This commit is contained in:
117
internal/worker/review.go
Normal file
117
internal/worker/review.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kamelion/ratatoskr-go/internal/storage"
|
||||
)
|
||||
|
||||
// maxReviewIterations — максимальное число раундов dev↔review.
|
||||
const maxReviewIterations = 3
|
||||
|
||||
// reviewVerdict — результат ревьюера (JSON из stdout).
|
||||
type reviewVerdict struct {
|
||||
Passed bool `json:"passed"`
|
||||
CriticalIssues []string `json:"critical_issues"`
|
||||
SolidViolations []string `json:"solid_violations"`
|
||||
Comments []string `json:"comments"`
|
||||
}
|
||||
|
||||
// repoDirOf возвращает абсолютный каталог репозитория в общем worktree.
|
||||
func (w *Worker) repoDirOf(repo string) string {
|
||||
return filepath.Join(w.Worktree, repo)
|
||||
}
|
||||
|
||||
// branchDiffAll собирает diff всей feature-ветки по каждому репо и возвращает
|
||||
// единый блок для ревьюера (с заголовками, чтобы reviewer понимал структуру).
|
||||
func (w *Worker) branchDiffAll(ctx context.Context, repos []string, branch string) (string, error) {
|
||||
var b strings.Builder
|
||||
for _, r := range repos {
|
||||
d, err := w.branchDiff(ctx, w.repoDirOf(r), branch)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrReviewDiff, err)
|
||||
}
|
||||
b.WriteString("\n===== DIFF repo: ")
|
||||
b.WriteString(r)
|
||||
b.WriteString(" (branch ")
|
||||
b.WriteString(branch)
|
||||
b.WriteString(") =====\n")
|
||||
b.WriteString(d)
|
||||
if strings.TrimSpace(d) == "" {
|
||||
b.WriteString("(нет изменений в этой ветке)\n")
|
||||
}
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
// pushBranches пушит feature-ветку во все затронутые репозитории.
|
||||
func (w *Worker) pushBranches(ctx context.Context, repos []string, branch string) error {
|
||||
for _, r := range repos {
|
||||
if err := w.pushBranch(ctx, w.repoDirOf(r), branch); err != nil {
|
||||
return fmt.Errorf("%w: repo %s: %v", ErrReviewPush, r, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// runReviewer разово запускает reviewer-агента и парсит JSON-вердикт.
|
||||
// Возвращает (verdict, output, traceID, err). err — только классы R2/R5;
|
||||
// непарсируемый JSON даёт (nil, output, traceID, nil) — вызывающий решает про retry.
|
||||
func (w *Worker) runReviewer(ctx context.Context, taskID int64, cwd, prompt string) (*reviewVerdict, string, int64, error) {
|
||||
tr := &storage.Trace{TaskID: taskID, Agent: "reviewer", Prompt: prompt}
|
||||
traceID, err := w.Store.AppendTrace(ctx, tr)
|
||||
if err != nil {
|
||||
return nil, "", 0, fmt.Errorf("%w: %v", ErrReviewTrace, err)
|
||||
}
|
||||
|
||||
res, resErr := w.Runner.Run(ctx, prompt, cwd, "reviewer", "")
|
||||
if resErr != nil {
|
||||
w.finalizeTrace(ctx, traceID, storage.TraceFailed, resErr.Error())
|
||||
return nil, "", traceID, fmt.Errorf("%w: %v", ErrReviewSpawn, resErr)
|
||||
}
|
||||
if res.SessionID != "" {
|
||||
_ = w.Store.UpdateTraceSessionID(ctx, traceID, res.SessionID)
|
||||
}
|
||||
|
||||
output := res.Stdout
|
||||
var v *reviewVerdict
|
||||
if res.RC == 0 {
|
||||
pv, perr := parseReviewVerdict(output)
|
||||
if perr == nil {
|
||||
v = pv
|
||||
}
|
||||
}
|
||||
|
||||
ts := storage.TraceFailed
|
||||
if v != nil && v.Passed {
|
||||
ts = storage.TraceSuccess
|
||||
}
|
||||
w.finalizeTrace(ctx, traceID, ts, output)
|
||||
|
||||
if v == nil {
|
||||
// Невалидный/пустой вывод или RC!=0: не парсится — сигнал на retry.
|
||||
return nil, output, traceID, nil
|
||||
}
|
||||
return v, output, traceID, nil
|
||||
}
|
||||
|
||||
// parseReviewVerdict извлекает JSON-объект вердикта из вывода reviewer,
|
||||
// устойчивый к случайному markdown-обрамлению.
|
||||
func parseReviewVerdict(out string) (*reviewVerdict, error) {
|
||||
s := strings.TrimSpace(out)
|
||||
if i := strings.Index(s, "{"); i >= 0 {
|
||||
s = s[i:]
|
||||
}
|
||||
if j := strings.LastIndex(s, "}"); j >= 0 {
|
||||
s = s[:j+1]
|
||||
}
|
||||
var v reviewVerdict
|
||||
if err := json.Unmarshal([]byte(s), &v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v, nil
|
||||
}
|
||||
Reference in New Issue
Block a user