package agents import ( "context" "encoding/json" "errors" "fmt" "log/slog" "net/http" "os" "path/filepath" "strings" "time" "g.fc.run/ai/byre/internal/api" "g.fc.run/ai/byre/internal/git" "g.fc.run/ai/byre/internal/ids" "g.fc.run/ai/byre/internal/mcp" "g.fc.run/ai/byre/internal/store" "g.fc.run/ai/byre/internal/workflow" ) const maxReviewFiles = 200 var severities = []string{"critical", "high", "medium", "low", "info"} const reviewGuidance = `You are a reviewer running inside byre. Another agent is doing a task; you review all of its work so far, and a human may read your review in byre's app. - Your working directory is a detached checkout of the snapshot under review. Read, build and run whatever you need there, but don't edit, commit or push: the checkout is discarded when you finish, and the task's own worktree belongs to its agent. - Report with byre's submit_review tool, exactly once, then end your turn with one line. A review that isn't submitted is lost. - Start the review with ` + "`change`" + `: what the change does, in a few sentences, for someone who hasn't read it. - Each finding says what is wrong, where (path and line), why it matters and what to do about it. Leave out praise and nits. No findings is a fine result. - With no findings, the summary is exactly "No significant findings." and nothing else. With findings, the summary is a bulleted Markdown list, one line per finding, and nothing else. - Show rather than quote. Give a finding's file and lines and byre shows that part of the diff, so don't paste code. When a picture would explain something better than prose, such as a data flow, call graph, state machine or schema the change alters, draw it in a ` + "```mermaid" + ` block. When the change alters something you can see, you can run it (on any free port on 127.0.0.1) and attach screenshots taken with the Playwright tools. Most reviews need neither; leave out a diagram or screenshot that adds nothing to the text.` func reviewSchema() json.RawMessage { images := map[string]any{ "type": "array", "description": "Screenshots, only when they show something the text can't.", "items": map[string]any{ "type": "object", "properties": map[string]any{ "path": map[string]any{"type": "string", "description": "A PNG, JPEG, GIF or WebP file of at most 5 MiB you saved: absolute, or relative to your working directory. byre keeps a copy."}, "caption": map[string]any{"type": "string", "description": "One line saying what it shows."}, }, "required": []any{"path"}, }, } b, err := json.Marshal(map[string]any{ "type": "object", "properties": map[string]any{ "change": map[string]any{"type": "string", "description": "What the change does, in a few sentences of Markdown, for someone who hasn't read it."}, "summary": map[string]any{"type": "string", "description": "Exactly \"No significant findings.\" when findings is empty; otherwise a bulleted Markdown list with one line per finding, and nothing else."}, "findings": map[string]any{ "type": "array", "items": map[string]any{ "type": "object", "properties": map[string]any{ "severity": map[string]any{"type": "string", "enum": severities}, "category": map[string]any{"type": "string", "description": "A short label, such as correctness, security or tests."}, "title": map[string]any{"type": "string", "description": "One line."}, "detail": map[string]any{"type": "string", "description": "What is wrong, why it matters and what to do, in Markdown."}, "path": map[string]any{"type": "string", "description": "Repo-relative path of the file, if the finding is about one."}, "line": map[string]any{"type": "integer", "description": "Line in the file as of this snapshot; byre shows the diff around it."}, "end_line": map[string]any{"type": "integer", "description": "Last line, when the finding covers a range."}, "images": images, }, "required": []any{"severity", "title", "detail"}, }, }, "images": images, }, "required": []any{"change", "summary", "findings"}, }) if err != nil { panic(err) } return b } func (m *Manager) reviewDir(projectID, reviewID string) string { return filepath.Join(m.worktreeRoot, projectID, "review-"+reviewID) } func (m *Manager) reviewMediaDir(projectID, reviewID string) string { return m.reviewDir(projectID, reviewID) + "-media" } func playwrightMCP(outputDir string) mcp.Stdio { return mcp.Stdio{ Name: "playwright", Command: "npx", Args: []string{"--yes", "@playwright/mcp@0.0.83", "--headless", "--isolated", "--browser", "chrome", "--file-paths", "absolute", "--output-dir", outputDir}, } } func (m *Manager) startReviews(ref store.SnapshotRef) { ctx := context.Background() m.reviewMu.Lock() defer m.reviewMu.Unlock() err := m.beginReviews(ctx, ref) if err != nil { slog.Error("start review", "task", ref.TaskID, "snapshot", ref.N, "err", err) } } func (m *Manager) beginReviews(ctx context.Context, ref store.SnapshotRef) error { task, project, err := m.activeTask(ctx, ref.TaskID) if err != nil { return err } snap, err := m.store.GetSnapshot(ctx, task.Id, ref.N) if err != nil { return err } now := time.Now().UTC() finished := func(state api.ReviewState, stage, problem string) error { return m.store.CreateReview(ctx, api.Review{Id: ids.New(ids.Review), TaskId: task.Id, SnapshotN: snap.N, Stage: stage, State: state, Error: problem, CreatedAt: now, FinishedAt: &now}, "") } if project.ReviewWorkflow == "" { return finished(api.ReviewStateFailed, "", fmt.Sprintf("project %s has no review workflow", project.Name)) } changes, err := git.Compare(project.RepoPath, project.DefaultBranch, snap.CommitSha, snap.TreeSha, "") if err != nil { return finished(api.ReviewStateFailed, project.ReviewWorkflow, fmt.Sprintf("byre couldn't compare snapshot %d with %s: %v", snap.N, project.DefaultBranch, err)) } if len(changes.Files) == 0 { slog.Info("review skipped, snapshot has no diff", "task", task.Id, "snapshot", snap.N) return finished(api.ReviewStateSkipped, "", "") } frozen, err := m.store.FreezeWorkflow(ctx, project.ReviewWorkflow) if err == nil { err = reviewProblem(frozen.Definition) } if err != nil { return finished(api.ReviewStateFailed, project.ReviewWorkflow, fmt.Sprintf("project %s reviews with workflow %q, which can't run: %v", project.Name, project.ReviewWorkflow, err)) } stages := []store.StageStart{} for _, stage := range workflow.Starts(frozen.Definition) { stages = append(stages, store.StageStart{TaskID: task.Id, SnapshotN: snap.N, DefinitionID: frozen.DefinitionID, Definition: frozen.Definition, Stage: stage}) } return m.startReviewers(ctx, task, project, snap, stages) } func (m *Manager) startReviewStage(s store.StageStart) { ctx := context.Background() m.reviewMu.Lock() defer m.reviewMu.Unlock() err := m.beginReviewStage(ctx, s) if err != nil { slog.Error("start review stage", "task", s.TaskID, "snapshot", s.SnapshotN, "stage", s.Stage, "err", err) } } func (m *Manager) beginReviewStage(ctx context.Context, s store.StageStart) error { task, project, err := m.activeTask(ctx, s.TaskID) if err != nil { return err } snap, err := m.store.GetSnapshot(ctx, task.Id, s.SnapshotN) if err != nil { return err } return m.startReviewers(ctx, task, project, snap, []store.StageStart{s}) } func (m *Manager) startReviewers(ctx context.Context, task api.Task, project api.Project, snap api.Snapshot, stages []store.StageStart) error { if len(stages) == 0 { return nil } now := time.Now().UTC() reviewers := []store.Reviewer{} failed := []api.Review{} prompts := map[string]string{} for _, s := range stages { model := workflow.Get(s.Definition, s.Stage).Model vendor := vendorOf(model) r := api.Review{Id: ids.New(ids.Review), TaskId: task.Id, SnapshotN: snap.N, Stage: s.Stage, Vendor: string(vendor), Model: model, CreatedAt: now} prompt, err := m.reviewPrompt(ctx, task, project, snap, s.Definition, s.Stage, s.Inputs) if err != nil { r.State = api.ReviewStateFailed r.Error = "the reviewer didn't start: " + err.Error() r.FinishedAt = &now failed = append(failed, r) continue } prompts[r.Id] = prompt reviewers = append(reviewers, store.Reviewer{ Session: api.AgentSession{ Id: ids.New(ids.Session), TaskId: task.Id, Vendor: vendor, Model: model, VendorSessionId: initialVendorSessionID(vendor), State: api.AgentSessionStateRunning, Role: api.Reviewer, CreatedBy: task.CreatedBy, CreatedAt: now, }, Review: r, }) } err := m.store.StartReviewers(ctx, project.Id, stages[0].DefinitionID, reviewers, failed) if err != nil { return err } for _, rv := range reviewers { m.launchReviewer(ctx, task, project, snap, rv, prompts[rv.Review.Id]) } return nil } func (m *Manager) launchReviewer(ctx context.Context, task api.Task, project api.Project, snap api.Snapshot, rv store.Reviewer, prompt string) { r := rv.Review err := git.AddDetachedWorktree(project.RepoPath, m.reviewDir(project.Id, r.Id), snap.CommitSha) if err == nil { err = os.MkdirAll(m.reviewMediaDir(project.Id, r.Id), 0o755) } if err == nil { _, err = m.launch(ctx, rv.Session, task, "byre", prompt, "", "", nil) } if err != nil { slog.Error("start reviewer", "review", r.Id, "task", task.Id, "stage", r.Stage, "err", err) m.failReview(ctx, r.Id, "the reviewer didn't start: "+err.Error()) m.endReviewer(ctx, rv.Session.Id, project, r.Id) return } slog.Info("review started", "review", r.Id, "task", task.Id, "snapshot", snap.N, "stage", r.Stage, "vendor", r.Vendor, "model", r.Model) } func (m *Manager) reviewPrompt(ctx context.Context, task api.Task, project api.Project, snap api.Snapshot, def api.WorkflowDefinition, stage string, inputs []api.Review) (string, error) { changes, err := git.Compare(project.RepoPath, project.DefaultBranch, snap.CommitSha, snap.TreeSha, "") if err != nil { return "", err } brief, err := m.store.FirstTaskMessage(ctx, task.Id) if err != nil { return "", err } b := &strings.Builder{} fmt.Fprintf(b, "Review the work of the agent on the byre task %q, as reviewer %q of the project's review workflow %q.\n\n%s\n\n", task.Title, stage, project.ReviewWorkflow, strings.TrimSpace(workflow.Get(def, stage).Prompt)) fmt.Fprintf(b, "It is snapshot %d of the task, commit %s, checked out in your working directory. The change is all of the task's work, `git diff %s %s` (from the merge-base with %s); %d files changed:\n", snap.N, snap.CommitSha, changes.Base, snap.CommitSha, project.DefaultBranch, len(changes.Files)) for i, f := range changes.Files { if i == maxReviewFiles { fmt.Fprintf(b, "- … and %d more\n", len(changes.Files)-maxReviewFiles) break } fmt.Fprintf(b, "- %s %s (+%d −%d)\n", f.Status, f.Path, f.Additions, f.Deletions) } if brief != "" { fmt.Fprintf(b, "\nThe task's brief, the first message its agent got:\n\n%s\n", quote(clip(brief, 4000))) } for _, in := range inputs { fmt.Fprintf(b, "\n## From reviewer %q\n\n%s\n", in.Stage, reviewText(in)) } b.WriteString("\nWhen you're done, call submit_review once with your review, then end your turn.") return b.String(), nil } func quote(s string) string { lines := strings.Split(strings.TrimSpace(s), "\n") for i, l := range lines { lines[i] = "> " + l } return strings.Join(lines, "\n") } func (m *Manager) submitReviewTool(l *live) mcp.Tool { return mcp.Tool{ Name: "submit_review", Description: "Submit your review. Call it exactly once, when you're done; its arguments are the review the human reads.", InputSchema: reviewSchema(), Handler: func(ctx context.Context, args json.RawMessage) (string, error) { return m.submitReview(ctx, l, args) }, } } func (m *Manager) submitReview(ctx context.Context, l *live, raw json.RawMessage) (string, error) { result := map[string]any{} err := json.Unmarshal(raw, &result) if err != nil { return "", fmt.Errorf("submit_review takes a JSON object: %w", err) } if c, ok := result["change"].(string); !ok || strings.TrimSpace(c) == "" { return "", errors.New("submit_review rejected, nothing was recorded: change must say what the change does") } if _, ok := result["summary"].(string); !ok { return "", errors.New("submit_review rejected, nothing was recorded: summary must be a string") } findings, ok := result["findings"].([]any) if !ok { return "", errors.New("submit_review rejected, nothing was recorded: findings must be a list") } holders := []map[string]any{result} for _, f := range findings { fm, ok := f.(map[string]any) if !ok { return "", errors.New("submit_review rejected, nothing was recorded: each finding must be an object") } holders = append(holders, fm) } pending := []reviewImage{} for _, holder := range holders { imgs, err := m.readReviewImages(l, holder["images"]) if err != nil { return "", fmt.Errorf("submit_review rejected, nothing was recorded: %w", err) } pending = append(pending, imgs...) } for _, img := range pending { b, err := m.store.CreateBlob(ctx, img.contentType, img.data, agentActor) if err != nil { return "", err } img.entry["blob"] = b.Id delete(img.entry, "path") } err = m.store.FinishReview(ctx, l.reviewID, api.ReviewStateDone, result, "") if errors.Is(err, store.ErrNotFound) { return "", errors.New("this review was already submitted or has ended") } if err != nil { return "", err } return "Review recorded. End your turn now with one line.", nil } func (m *Manager) settleReviews(ref store.SnapshotRef) { ctx := context.Background() m.settle(ctx, ref) err := m.store.FinishSettling(ctx, ref.TaskID, ref.N) if err != nil { slog.Error("finish settling reviews", "task", ref.TaskID, "snapshot", ref.N, "err", err) return } m.poke() } func (m *Manager) settle(ctx context.Context, ref store.SnapshotRef) { reviews, err := m.store.SnapshotReviews(ctx, ref.TaskID, ref.N) if err != nil { slog.Error("list reviews to settle", "task", ref.TaskID, "err", err) return } reviewIDs := []string{} for _, r := range reviews { reviewIDs = append(reviewIDs, r.Id) } m.deliverFindings(ctx, ref, reviews) mar, err := m.store.TakeMergeAfterReview(ctx, ref.TaskID, reviewIDs) if err != nil { slog.Error("take merge after review", "task", ref.TaskID, "err", err) return } if mar.By != "" { m.mergeAfterReview(ctx, ref, reviews, mar) } } func (m *Manager) deliverFindings(ctx context.Context, ref store.SnapshotRef, reviews []api.Review) { text := findingsMessage(ref.N, reviews) if text == "" { return } latest, err := m.store.LastReviewable(ctx, ref.TaskID) if err != nil || latest.N != ref.N { return } s, found, err := m.liveSession(ctx, ref.TaskID) if err != nil { slog.Error("list sessions to deliver findings", "task", ref.TaskID, "err", err) return } if !found { slog.Info("review findings not delivered, task has no live agent", "task", ref.TaskID, "snapshot", ref.N) return } _, _, err = m.Message(ctx, s.Id, "byre", text, "", "", "") if err != nil { slog.Error("deliver review findings", "task", ref.TaskID, "snapshot", ref.N, "session", s.Id, "err", err) return } slog.Info("review findings delivered", "task", ref.TaskID, "snapshot", ref.N, "session", s.Id) } const findingsPreamble = "The review of snapshot #" func isFindingsMessage(actor, text string) bool { return actor == "byre" && strings.HasPrefix(text, findingsPreamble) } func findingsMessage(n int, reviews []api.Review) string { b := &strings.Builder{} for _, r := range reviews { if r.State != api.ReviewStateDone || r.Result == nil { continue } findings, _ := (*r.Result)["findings"].([]any) if len(findings) == 0 { continue } fmt.Fprintf(b, "\n\n## Reviewer %q", r.Stage) for i, f := range findings { fm, _ := f.(map[string]any) where := "" if p, ok := fm["path"].(string); ok && p != "" { where = p if line, ok := fm["line"].(float64); ok && line > 0 { where += fmt.Sprintf(":%d", int(line)) } where = " (`" + where + "`)" } fmt.Fprintf(b, "\n\n**Finding %d: [%v] %v**%s\n\n%s", i+1, fm["severity"], fm["title"], where, strings.TrimSpace(fmt.Sprint(fm["detail"]))) } } if b.Len() == 0 { return "" } return fmt.Sprintf(findingsPreamble+"%d of your work has finished, and its reviewers raised the findings below. Reviewers can be wrong or miss context: check each one against the code and your brief. "+ "Before you change anything, answer each finding with respond_to_finding (snapshot %d, the reviewer's name and the finding's number): fixing if it holds up and you'll act on it, ignoring with your reason if you're leaving it. "+ "The human sees your answers on the findings and can overrule an ignore.", n, n) + b.String() } type findingResponseArgs struct { Snapshot int `json:"snapshot"` Reviewer string `json:"reviewer"` Finding int `json:"finding"` Response api.FindingResponseResponse `json:"response"` Reason string `json:"reason"` } func (m *Manager) respondToFindingTool(l *live) mcp.Tool { return mcp.Tool{ Name: "respond_to_finding", Description: "Answer one finding of a review of your work, as soon as you've decided what to do about it: fixing when you'll act on it, ignoring when you're leaving it. " + "The human sees your answer on the finding; an ignore keeps the human's fix button, so they can overrule you. Call it again to change your answer.", InputSchema: json.RawMessage(`{ "type": "object", "properties": { "snapshot": {"type": "integer", "description": "The reviewed snapshot's number."}, "reviewer": {"type": "string", "description": "The reviewer's name, as the findings message gives it."}, "finding": {"type": "integer", "description": "The finding's number in that reviewer's review, from 1."}, "response": {"type": "string", "enum": ["fixing", "ignoring"]}, "reason": {"type": "string", "description": "One line: what you'll do about it, or why you're leaving it."} }, "required": ["snapshot", "reviewer", "finding", "response", "reason"] }`), Handler: func(ctx context.Context, raw json.RawMessage) (string, error) { args := findingResponseArgs{} err := json.Unmarshal(raw, &args) if err != nil { return "", fmt.Errorf("malformed respond_to_finding call: %w", err) } if !args.Response.Valid() { return "", fmt.Errorf("%w: response must be fixing or ignoring", ErrInvalid) } if args.Finding < 1 { return "", fmt.Errorf("%w: finding numbers start at 1", ErrInvalid) } reason := strings.TrimSpace(args.Reason) if reason == "" { return "", fmt.Errorf("%w: reason must not be empty", ErrInvalid) } reviews, err := m.store.SnapshotReviews(ctx, l.taskID, args.Snapshot) if err != nil { return "", err } reviewID := "" for _, r := range reviews { if r.Stage == args.Reviewer && r.State == api.ReviewStateDone { reviewID = r.Id } } if reviewID == "" { return "", fmt.Errorf("%w: snapshot %d has no finished review by reviewer %q", ErrInvalid, args.Snapshot, args.Reviewer) } err = m.store.SetFindingResponse(ctx, reviewID, args.Finding-1, api.FindingResponse{Response: args.Response, Reason: reason}, agentActor) if errors.Is(err, store.ErrNotFound) { return "", fmt.Errorf("%w: reviewer %q's review of snapshot %d has no finding %d", ErrInvalid, args.Reviewer, args.Snapshot, args.Finding) } if err != nil { return "", err } return fmt.Sprintf("Recorded: %s finding %d of reviewer %q's review of snapshot %d.", args.Response, args.Finding, args.Reviewer, args.Snapshot), nil }, } } type reviewImage struct { entry map[string]any contentType string data []byte } func (m *Manager) readReviewImages(l *live, raw any) ([]reviewImage, error) { if raw == nil { return nil, nil } list, ok := raw.([]any) if !ok { return nil, errors.New("images must be a list") } out := []reviewImage{} for _, item := range list { entry, ok := item.(map[string]any) if !ok { return nil, errors.New("each image must be an object with a path") } p, ok := entry["path"].(string) if !ok || p == "" { return nil, errors.New("each image needs a path") } if !filepath.IsAbs(p) { p = filepath.Join(l.dir, p) } info, err := os.Stat(p) if err != nil { return nil, fmt.Errorf("image %s: %w", p, err) } if !info.Mode().IsRegular() { return nil, fmt.Errorf("image %s is not a file", p) } if info.Size() > store.MaxBlobBytes { return nil, fmt.Errorf("image %s is %d bytes; images are limited to %d MiB", p, info.Size(), store.MaxBlobBytes>>20) } data, err := os.ReadFile(p) if err != nil { return nil, fmt.Errorf("image %s: %w", p, err) } contentType := http.DetectContentType(data) if !store.ImageTypes[contentType] { return nil, fmt.Errorf("image %s is %s; use PNG, JPEG, GIF or WebP", p, contentType) } out = append(out, reviewImage{entry: entry, contentType: contentType, data: data}) } return out, nil } func (m *Manager) reviewTurnEnded(ctx context.Context, l *live, res endResult) error { m.mu.Lock() interrupted := l.interrupted m.mu.Unlock() err := m.emit(ctx, l, agentActor, "turn.ended", res.payload) if err != nil { return err } if res.window > 0 { err = m.updateContext(ctx, l, l.contextUsed, res.window) if err != nil { return err } } reason := fmt.Sprintf("the reviewer ended its turn without calling submit_review: %s", clip(strings.TrimSpace(res.result), 500)) switch { case interrupted: reason = "the reviewer was interrupted" case res.isError: reason = fmt.Sprintf("the reviewer's turn ended with an error (%s): %s", res.subtype, clip(res.result, 500)) } go m.finishReviewer(l, reason) return nil } func (m *Manager) finishReviewer(l *live, reason string) { ctx := context.Background() project, err := m.store.GetProject(ctx, l.projectID) if err != nil { slog.Error("load project to end reviewer", "review", l.reviewID, "err", err) return } m.failReview(ctx, l.reviewID, reason) m.endReviewer(ctx, l.id, project, l.reviewID) } func (m *Manager) endReviewer(ctx context.Context, sessionID string, project api.Project, reviewID string) { sess, err := m.store.GetSession(ctx, sessionID) if err != nil { slog.Error("load reviewer session", "session", sessionID, "err", err) } if err == nil && sess.State != api.AgentSessionStateCompleted { err = m.retire(ctx, sess, "byre") if err != nil { slog.Error("retire reviewer session", "session", sessionID, "err", err) } } m.removeReviewDir(project, reviewID) } func (m *Manager) cleanUpReviewer(r store.EndedReviewer) { m.stopLive(r.SessionID) m.removeReviewDir(api.Project{Id: r.ProjectID, RepoPath: r.RepoPath}, r.ReviewID) } func (m *Manager) removeReviewDir(project api.Project, reviewID string) { err := git.RemoveDetachedWorktree(project.RepoPath, m.reviewDir(project.Id, reviewID)) if err != nil { slog.Error("remove review worktree", "review", reviewID, "err", err) } err = os.RemoveAll(m.reviewMediaDir(project.Id, reviewID)) if err != nil { slog.Error("remove review media", "review", reviewID, "err", err) } } func (m *Manager) failReview(ctx context.Context, reviewID, reason string) { err := m.store.FinishReview(ctx, reviewID, api.ReviewStateFailed, nil, reason) if errors.Is(err, store.ErrNotFound) { return } if err != nil { slog.Error("record failed review", "review", reviewID, "err", err) return } slog.Info("review failed", "review", reviewID, "reason", reason) } func (m *Manager) StopReview(ctx context.Context, reviewID, reason string) (api.Review, error) { m.reviewMu.Lock() defer m.reviewMu.Unlock() r, err := m.store.GetReview(ctx, reviewID) if err != nil { return api.Review{}, err } if r.State != api.ReviewStateRunning { return api.Review{}, fmt.Errorf("%w: review %s is %s, not running", ErrConflict, r.Id, r.State) } task, err := m.store.GetTask(ctx, r.TaskId) if err != nil { return api.Review{}, err } project, err := m.store.GetProject(ctx, task.ProjectId) if err != nil { return api.Review{}, err } m.failReview(ctx, r.Id, reason) m.endReviewer(ctx, r.AgentSessionId, project, r.Id) return m.store.GetReview(ctx, r.Id) } func (m *Manager) stopStuckReviewers(ctx context.Context, h Health) { for _, p := range h.Processes { if p.Health != api.HealthStuck { continue } m.mu.Lock() l := m.live[p.SessionId] reviewID := "" if l != nil { reviewID = l.reviewID } m.mu.Unlock() if reviewID == "" { continue } _, err := m.StopReview(ctx, reviewID, fmt.Sprintf("the reviewer went %d minutes without an event", int(StuckAfter/time.Minute))) if err != nil && !errors.Is(err, ErrConflict) { slog.Error("stop stuck reviewer", "review", reviewID, "session", p.SessionId, "err", err) } } } func reviewName(r api.Review) string { if r.Stage == "" { return fmt.Sprintf("review of snapshot %d", r.SnapshotN) } return fmt.Sprintf("%q review of snapshot %d", r.Stage, r.SnapshotN) } func reviewLine(r api.Review) string { if r.State == api.ReviewStateFailed { return fmt.Sprintf("%s failed: %s", reviewName(r), clip(r.Error, 300)) } if r.State == api.ReviewStateSkipped { return fmt.Sprintf("%s skipped: no changes", reviewName(r)) } res := map[string]any{} if r.Result != nil { res = *r.Result } b := &strings.Builder{} fmt.Fprintf(b, "%s: %s", reviewName(r), clip(strings.TrimSpace(fmt.Sprint(res["summary"])), 600)) if c, ok := res["change"].(string); ok && c != "" { fmt.Fprintf(b, "\n change: %s", clip(strings.TrimSpace(c), 600)) } writeFindings(b, res, "\n ", 300) return b.String() } func reviewText(r api.Review) string { res := map[string]any{} if r.Result != nil { res = *r.Result } b := &strings.Builder{} b.WriteString(strings.TrimSpace(fmt.Sprint(res["summary"]))) writeFindings(b, res, "\n", 4000) return b.String() } func writeFindings(b *strings.Builder, res map[string]any, lead string, limit int) { findings, _ := res["findings"].([]any) for _, f := range findings { fm, _ := f.(map[string]any) where := "" if p, ok := fm["path"].(string); ok && p != "" { where = " (" + p if line, ok := fm["line"].(float64); ok && line > 0 { where += fmt.Sprintf(":%d", int(line)) } where += ")" } fmt.Fprintf(b, "%s- [%v] %v%s: %s", lead, fm["severity"], fm["title"], where, clip(strings.TrimSpace(fmt.Sprint(fm["detail"])), limit)) } }