Files
byre/internal/agents/audit.go
T

147 lines
4.3 KiB
Go
Raw Normal View History

package agents
import (
"context"
"fmt"
"log/slog"
"strings"
"g.fc.run/ai/byre/internal/api"
"g.fc.run/ai/byre/internal/git"
)
func (m *Manager) SetAuditWorkflow(ctx context.Context, projectID, name, actor string) (api.Project, error) {
name = strings.TrimSpace(name)
if name != "" {
_, err := m.store.GetWorkflow(ctx, name)
if err != nil {
return api.Project{}, err
}
project, err := m.store.GetProject(ctx, projectID)
if err != nil {
return api.Project{}, err
}
creator, err := m.store.GetUser(ctx, project.CreatedBy)
if err != nil {
return api.Project{}, err
}
if creator.GitName == "" || creator.GitEmail == "" {
return api.Project{}, fmt.Errorf("%w: audits run as the project's creator %s, who has no git identity", ErrInvalid, creator.Login)
}
}
p, err := m.store.SetAuditWorkflow(ctx, projectID, name, actor)
if err != nil {
return api.Project{}, err
}
go m.audit(p.Id)
return p, nil
}
func (m *Manager) auditAll(ctx context.Context) {
projects, err := m.store.ListProjects(ctx)
if err != nil {
slog.Error("start audits", "err", err)
return
}
for _, p := range projects {
tasks, err := m.store.ListTasks(ctx, p.Id)
if err != nil {
slog.Error("start audits", "project", p.Id, "err", err)
continue
}
for _, t := range tasks {
if t.Status == api.TaskStatusActive && t.Background && t.State == api.TaskStateDiscardSuggested {
m.dismissAudit(t.Id)
}
}
m.audit(p.Id)
}
}
func (m *Manager) audit(projectID string) {
err := m.startAudit(context.Background(), projectID)
if err != nil {
slog.Error("start audit", "project", projectID, "err", err)
}
}
func (m *Manager) startAudit(ctx context.Context, projectID string) error {
m.auditMu.Lock()
defer m.auditMu.Unlock()
project, err := m.store.GetProject(ctx, projectID)
if err != nil || project.AuditWorkflow == "" {
return err
}
last, active, err := m.store.LastAudit(ctx, projectID)
if err != nil || active {
return err
}
head, err := git.BranchHead(project.RepoPath, project.DefaultBranch)
if err != nil {
return err
}
if head == last {
return nil
}
creator, err := m.store.GetUser(ctx, project.CreatedBy)
if err != nil {
return err
}
tasks, err := m.StartWorkflow(ctx, project, creator, "Audit of "+short(head), head, "byre", project.AuditWorkflow, auditBrief(project.DefaultBranch, head), nil, head)
if err == nil {
slog.Info("audit started", "project", projectID, "commit", head, "workflow", project.AuditWorkflow)
return nil
}
for _, t := range tasks {
m.auditError(ctx, t, fmt.Sprintf("The audit of %s didn't start: %v. Discard this task to let audits continue; the next commit on %s starts one.", short(head), err, project.DefaultBranch))
}
return err
}
func (m *Manager) auditError(ctx context.Context, task api.Task, msg string) {
err := m.store.AppendEvent(ctx, api.Event{
Kind: "session.error",
ProjectId: &task.ProjectId,
TaskId: &task.Id,
Actor: "byre",
Payload: map[string]any{"error": msg},
})
if err != nil {
slog.Error("record audit error", "task", task.Id, "err", err)
}
}
func auditBrief(branch, commit string) string {
return fmt.Sprintf("Audit of %s at %s. This isn't a review of a change: check the whole codebase as it stands against the project's standards and expectations, and fix what falls short. "+
"byre runs this task in the background: the human doesn't see it until you ask a question, call ready_for_merge or hit an error. "+
"Finish your stage with finish_stage as usual, even when you found nothing. Once the workflow has finished, byre asks whether the task is ready to merge; "+
"if nothing needed changing, answer with suggest_discard, and byre discards the task without bothering the human.", branch, commit)
}
func (m *Manager) dismissAudit(taskID string) {
ctx := context.Background()
task, err := m.store.GetTask(ctx, taskID)
if err != nil {
slog.Error("dismiss audit", "task", taskID, "err", err)
return
}
if !task.Background || task.Status != api.TaskStatusActive {
return
}
finished, err := m.store.WorkflowFinished(ctx, *task.WorkflowRun)
if err != nil {
slog.Error("dismiss audit", "task", taskID, "err", err)
return
}
if !finished {
return
}
_, err = m.Discard(ctx, task, "byre")
if err != nil {
slog.Error("dismiss audit", "task", taskID, "err", err)
return
}
slog.Info("audit dismissed itself", "task", taskID)
}