147 lines
4.3 KiB
Go
147 lines
4.3 KiB
Go
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)
|
|
}
|