Files
ratatoskr-go/internal/worker/review.go
Hermes da775ac227
All checks were successful
CI / test (push) Successful in 54s
CI / build-and-package (amd64, linux) (push) Successful in 35s
CI / build-and-package (amd64, windows) (push) Successful in 43s
feat: наблюдение за живой opencode-сессией (/status N показывает что делает агент)
LiveRegistry собирает live-шаги из stdout процесса opencode по taskID
(через контекст, без изменения интерфейса Runner). /status N для running-задачи
прикладывает живое состояние: активный субагент, давность последнего шага,
последние text-шаги агента. Ничего не перезапускается и не убивается —
только наблюдение по запросу.
2026-08-17 08:05:15 +05:00

118 lines
3.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(w.runCtx(ctx, taskID), 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
}