package agents import ( "context" "encoding/json" "errors" "fmt" "log/slog" "strings" "g.fc.run/ai/byre/internal/api" "g.fc.run/ai/byre/internal/git" "g.fc.run/ai/byre/internal/mcp" "g.fc.run/ai/byre/internal/store" "g.fc.run/ai/byre/internal/workflow" ) func (m *Manager) SaveWorkflow(ctx context.Context, name, from string, definition []byte, actor string) (api.WorkflowSummary, error) { name, err := workflow.CleanName(name) if err != nil { return api.WorkflowSummary{}, err } def, err := workflow.Parse(definition) if err != nil { return api.WorkflowSummary{}, err } err = m.store.PutWorkflow(ctx, name, from, def, actor) if errors.Is(err, store.ErrWorkflowExists) { return api.WorkflowSummary{}, fmt.Errorf("%w: %w", ErrConflict, err) } if err != nil { return api.WorkflowSummary{}, err } return api.WorkflowSummary{Name: name, Description: def.Description}, nil } func (m *Manager) SetReviewWorkflow(ctx context.Context, projectID, name, actor string) (api.Project, error) { name = strings.TrimSpace(name) if name != "" { def, err := m.store.GetWorkflow(ctx, name) if err != nil { return api.Project{}, err } err = reviewProblem(def) if err != nil { return api.Project{}, fmt.Errorf("%w: workflow %q can't review: %w", ErrInvalid, name, err) } } return m.store.SetReviewWorkflow(ctx, projectID, name, actor) } func reviewProblem(def api.WorkflowDefinition) error { for _, s := range workflow.Stages(def) { if s.Agents != nil { return fmt.Errorf("stage %q is a team, and each review stage is one reviewer", s.Stage) } } return nil } func (m *Manager) TaskWorkflow(ctx context.Context, taskID string) (api.TaskWorkflow, error) { frozen, err := m.store.TaskWorkflow(ctx, taskID) if err != nil { return api.TaskWorkflow{}, err } return api.TaskWorkflow{Name: frozen.Name, Definition: frozen.Definition}, nil } func (m *Manager) WorkflowRun(ctx context.Context, taskID string) (api.WorkflowRun, error) { frozen, err := m.store.TaskWorkflow(ctx, taskID) if err != nil { return api.WorkflowRun{}, err } task, err := m.store.GetTask(ctx, taskID) if err != nil { return api.WorkflowRun{}, err } tasks, err := m.store.RunTasks(ctx, *task.WorkflowRun) if err != nil { return api.WorkflowRun{}, err } events, err := m.store.RunEvents(ctx, *task.WorkflowRun) if err != nil { return api.WorkflowRun{}, err } run := api.WorkflowRun{Name: frozen.Name, Run: *task.WorkflowRun, Title: frozen.Title, Brief: frozen.Brief, Definition: frozen.Definition, Stages: []api.WorkflowRunStage{}} after := map[string][]string{} discardedBy := map[string]string{} for _, ev := range events { switch ev.Kind { case "task.workflow_stage": ids := []string{} list, _ := ev.Payload["after"].([]any) for _, v := range list { id, _ := v.(string) ids = append(ids, id) } after[*ev.TaskId] = ids case "task.discarded": discardedBy[*ev.TaskId] = ev.Actor case "task.workflow_finished": run.Finished = true } } byID := map[string]api.Task{} for _, t := range tasks { byID[t.Id] = t } var inputs func(id string) []api.StageInput inputs = func(id string) []api.StageInput { ids, found := after[id] if !found { t := byID[id] if t.ForkedFrom == nil { return []api.StageInput{} } return inputs(*t.ForkedFrom) } in := []api.StageInput{} for _, a := range ids { stage := "" if t, found := byID[a]; found { stage = *t.WorkflowStage } in = append(in, api.StageInput{Stage: stage, TaskId: a}) } return in } attempts := map[string][]api.StageAttempt{} done := map[string]bool{} for _, t := range tasks { output, commit, err := m.store.StageResult(ctx, t.Id) if err != nil { return api.WorkflowRun{}, err } running := false if t.Status == api.TaskStatusActive { sessions, err := m.store.ListSessions(ctx, t.Id) if err != nil { return api.WorkflowRun{}, err } for _, s := range sessions { running = running || s.State == api.AgentSessionStateRunning } } a := api.StageAttempt{ TaskId: t.Id, Title: t.Title, State: attemptState(t, output != "", running), Discarding: t.Discarding, ForkedFrom: t.ForkedFrom, Inputs: inputs(t.Id), } if output != "" { a.Output = &output a.Commit = &commit done[*t.WorkflowStage] = true } if by, found := discardedBy[t.Id]; found && t.Status == api.TaskStatusDiscarded { a.DiscardedBy = &by } attempts[*t.WorkflowStage] = append(attempts[*t.WorkflowStage], a) } for _, s := range workflow.Stages(frozen.Definition) { stage := api.WorkflowRunStage{ Name: s.Stage, After: workflow.After(frozen.Definition, s.Stage), State: api.StageStateWaiting, WaitingOn: []string{}, Attempts: attempts[s.Stage], } if stage.Attempts == nil { stage.Attempts = []api.StageAttempt{} } for _, a := range stage.After { if !done[a] { stage.WaitingOn = append(stage.WaitingOn, a) } } if len(stage.Attempts) > 0 { deciding := stage.Attempts[len(stage.Attempts)-1] for _, a := range stage.Attempts { if a.Output != nil { deciding = a } } stage.State = api.WorkflowRunStageState(deciding.State) } run.Stages = append(run.Stages, stage) } return run, nil } func attemptState(t api.Task, finished, running bool) api.StageAttemptState { switch { case t.Status == api.TaskStatusMerged: return api.AttemptStateMerged case t.Status == api.TaskStatusDiscarded && finished: return api.AttemptStateCleanedUp case t.Status == api.TaskStatusDiscarded: return api.AttemptStateDiscarded case finished: return api.AttemptStateFinished case running: return api.AttemptStateRunning } return api.AttemptStateIdle } var errModelWithWorkflow = fmt.Errorf("%w: model can't be combined with workflow: each stage names its own model", ErrInvalid) type parentStage struct { task api.Task output string commit string } func (m *Manager) StartWorkflow(ctx context.Context, project api.Project, creator api.User, title, base, actor, name, brief string, todoItem *string, audit string) ([]api.Task, error) { if creator.GitName == "" || creator.GitEmail == "" { return nil, ErrNoGitIdentity } frozen, err := m.store.FreezeWorkflow(ctx, name) if err != nil { return nil, err } frozen.Brief = brief frozen.Title = title frozen.Audit = audit run := store.NewID() tasks := []api.Task{} for _, stage := range workflow.Starts(frozen.Definition) { task, err := m.startStage(ctx, project, creator, frozen, run, stage, base, todoItem, nil, actor) if task.Id != "" { tasks = append(tasks, task) } if err != nil { return tasks, fmt.Errorf("workflow %q: stage %q did not start: %w", name, stage, err) } } return tasks, nil } func (m *Manager) startStage(ctx context.Context, project api.Project, creator api.User, frozen store.FrozenWorkflow, run, stage, base string, todoItem *string, parents []parentStage, actor string) (api.Task, error) { t := api.Task{ Title: fmt.Sprintf("%s: %s", frozen.Title, stage), MergeTitle: frozen.Title, CreatedBy: creator.Login, TodoItem: todoItem, WorkflowRun: &run, WorkflowStage: &stage, } if frozen.Audit != "" { t.Audit = &frozen.Audit } task, err := m.createTask(ctx, project, t, &frozen, base, actor) if err != nil { return api.Task{}, err } _, err = m.beginStage(ctx, task, project, creator, frozen, parents, actor) if err != nil { return task, fmt.Errorf("task %s created, but its agent did not start: %w", task.Id, err) } return task, nil } func (m *Manager) beginStage(ctx context.Context, task api.Task, project api.Project, creator api.User, frozen store.FrozenWorkflow, parents []parentStage, actor string) (api.AgentSession, error) { stage := *task.WorkflowStage after := []string{} for _, p := range parents { after = append(after, p.task.Id) } err := m.store.AppendEvent(ctx, api.Event{ Kind: "task.workflow_stage", ProjectId: &task.ProjectId, TaskId: &task.Id, Actor: actor, Payload: map[string]any{"id": task.Id, "workflow": frozen.Name, "run": *task.WorkflowRun, "stage": stage, "after": after}, }) if err != nil { return api.AgentSession{}, err } s := workflow.Get(frozen.Definition, stage) message := stageMessage(frozen, stage, parents, project.DefaultBranch) if s.Agents == nil { return m.create(ctx, task, creator, "", message, s.Model, "byre", "", nil) } err = m.startTeam(ctx, task, creator, message, *s.Agents, "byre") if err != nil { return api.AgentSession{}, err } sess, _, err := m.liveSession(ctx, task.Id) return sess, err } func stageMessage(frozen store.FrozenWorkflow, stage string, parents []parentStage, defaultBranch string) string { b := &strings.Builder{} fmt.Fprintf(b, "Workflow %q, stage %q\n\n%s", frozen.Name, stage, strings.TrimSpace(workflow.Get(frozen.Definition, stage).Prompt)) if strings.TrimSpace(frozen.Brief) != "" { fmt.Fprintf(b, "\n\n## Brief\n\n%s", strings.TrimSpace(frozen.Brief)) } for _, p := range parents { fmt.Fprintf(b, "\n\n## From stage %q (task %s, branch %s)\n\n%s", *p.task.WorkflowStage, p.task.Id, p.task.Branch, strings.TrimSpace(p.output)) } switch { case len(parents) == 0: case len(parents) > 1: fmt.Fprintf(b, "\n\nThis task starts from %s. The branch of each stage above holds that stage's work: `git merge` the ones this stage needs, or read them with `git show` and `git diff`.", defaultBranch) default: fmt.Fprintf(b, "\n\nThis task starts from the commit stage %q finished at, so its work is already in this worktree.", *parents[0].task.WorkflowStage) } return b.String() } func (m *Manager) recordStage(ctx context.Context, l *live) error { output, err := m.store.StageOutput(ctx, l.id) if err != nil { return err } head, err := git.Head(l.dir) if err != nil { return err } return m.store.RecordStage(ctx, l.taskID, output, head) } func (m *Manager) advanceStage(l *live) { ctx := context.Background() _, err := m.nextStage(ctx, l.id) if err != nil { slog.Error("advance workflow stage", "session", l.id, "err", err) m.recordError(l, fmt.Sprintf("The workflow didn't move on from this stage: %v. byre tries again when it next starts.", err)) } m.afterTurn(ctx, l) } func (m *Manager) recoverStages(ctx context.Context) (map[string]bool, error) { ids, err := m.store.FinishedStages(ctx) if err != nil { return nil, err } started := map[string]bool{} for _, id := range ids { next, err := m.nextStage(ctx, id) if err != nil { slog.Error("advance workflow stage", "session", id, "err", err) continue } for _, n := range next { started[n] = true } slog.Info("advanced workflow stage", "session", id, "next", next) } return started, nil } func (m *Manager) nextStage(ctx context.Context, sessionID string) ([]string, error) { sess, err := m.store.GetSession(ctx, sessionID) if err != nil { return nil, err } task, err := m.store.GetTask(ctx, sess.TaskId) if err != nil { return nil, err } discard, err := m.store.GetDiscard(ctx, task.Id) if err != nil { return nil, err } if task.Status != api.TaskStatusActive || discard.State == store.DiscardClosing { return nil, m.store.ClearStage(ctx, sess.Id) } frozen, err := m.store.TaskWorkflow(ctx, task.Id) if err != nil { return nil, err } output, err := m.store.StageOutput(ctx, sess.Id) if err != nil { return nil, err } recorded, _, err := m.store.StageResult(ctx, task.Id) if err != nil { return nil, err } if recorded == "" { head, err := git.Head(task.WorktreePath) if err != nil { return nil, err } err = m.store.RecordStage(ctx, task.Id, output, head) if err != nil { return nil, err } } m.workflowMu.Lock() defer m.workflowMu.Unlock() next := workflow.Next(frozen.Definition, *task.WorkflowStage) if len(next) == 0 { finished, err := m.store.WorkflowFinished(ctx, *task.WorkflowRun) if err != nil { return nil, err } if finished { return nil, m.store.ClearStage(ctx, sess.Id) } tasks, err := m.store.RunTasks(ctx, *task.WorkflowRun) if err != nil { return nil, err } keep := map[string]bool{} kept := []string{} outputs := []string{} for _, end := range workflow.Ends(frozen.Definition) { p, found, err := m.finishedStage(ctx, tasks, end) if err != nil { return nil, err } if !found { return nil, m.store.ClearStage(ctx, sess.Id) } keep[p.task.Id] = true kept = append(kept, p.task.Id) outputs = append(outputs, fmt.Sprintf("## %s\n\n%s", end, strings.TrimSpace(p.output))) } if len(outputs) > 1 { output = strings.Join(outputs, "\n\n") } err = m.discardStages(ctx, tasks, keep) if err != nil { return nil, err } return nil, m.store.FinishWorkflow(ctx, task, sess.Id, frozen.Name, output, kept) } started := []string{} for _, n := range next { ids, err := m.advanceTo(ctx, task, frozen, n) if err != nil { return started, err } started = append(started, ids...) } return started, m.store.ClearStage(ctx, sess.Id) } func (m *Manager) advanceTo(ctx context.Context, from api.Task, frozen store.FrozenWorkflow, stage string) ([]string, error) { tasks, err := m.store.RunTasks(ctx, *from.WorkflowRun) if err != nil { return nil, err } parents := []parentStage{} for _, a := range workflow.After(frozen.Definition, stage) { p, found, err := m.finishedStage(ctx, tasks, a) if err != nil || !found { return nil, err } parents = append(parents, p) } project, err := m.store.GetProject(ctx, from.ProjectId) if err != nil { return nil, err } creator, err := m.store.GetUser(ctx, from.CreatedBy) if err != nil { return nil, err } existing, found := latestStage(tasks, stage) if found { if existing.Status != api.TaskStatusActive || existing.Discarding { return nil, nil } return m.resumeStage(ctx, existing, project, creator, frozen, parents) } base := parents[0].commit switch { case len(parents) == 1: case frozen.Audit != "": base = frozen.Audit default: base = project.DefaultBranch } task, err := m.startStage(ctx, project, creator, frozen, *from.WorkflowRun, stage, base, from.TodoItem, parents, "byre") if err != nil { return nil, err } return m.sessionIDs(ctx, task.Id) } func (m *Manager) sessionIDs(ctx context.Context, taskID string) ([]string, error) { sessions, err := m.store.ListSessions(ctx, taskID) if err != nil { return nil, err } ids := []string{} for _, s := range sessions { ids = append(ids, s.Id) } return ids, nil } func latestStage(tasks []api.Task, stage string) (api.Task, bool) { for i := len(tasks) - 1; i >= 0; i-- { if *tasks[i].WorkflowStage == stage { return tasks[i], true } } return api.Task{}, false } func (m *Manager) finishedStage(ctx context.Context, tasks []api.Task, stage string) (parentStage, bool, error) { for i := len(tasks) - 1; i >= 0; i-- { if *tasks[i].WorkflowStage != stage { continue } output, commit, err := m.store.StageResult(ctx, tasks[i].Id) if err != nil { return parentStage{}, false, err } if output != "" { return parentStage{task: tasks[i], output: output, commit: commit}, true, nil } } return parentStage{}, false, nil } func (m *Manager) resumeStage(ctx context.Context, task api.Task, project api.Project, creator api.User, frozen store.FrozenWorkflow, parents []parentStage) ([]string, error) { sessions, err := m.store.ListSessions(ctx, task.Id) if err != nil { return nil, err } if len(sessions) == 0 { _, err := m.beginStage(ctx, task, project, creator, frozen, parents, "byre") if err != nil { return nil, err } return m.sessionIDs(ctx, task.Id) } s := workflow.Get(frozen.Definition, *task.WorkflowStage) ids := []string{} for _, sess := range sessions { started, err := m.store.SessionStarted(ctx, sess.Id) if err != nil { return ids, err } if started || sess.State == api.AgentSessionStateCompleted { continue } if sess.State == api.AgentSessionStateRunning { err = m.store.SetSessionState(ctx, sess.Id, api.AgentSessionStateIdle, "byre", nil) if err != nil { return ids, err } } message := stageMessage(frozen, *task.WorkflowStage, parents, project.DefaultBranch) if s.Agents != nil { mem, err := teamMember(*s.Agents, sess.Member) if err != nil { return ids, err } message = teamPrompt(message, mem) } _, err = m.turn(ctx, sess, task, "byre", message, "", nil) if err != nil { return ids, err } ids = append(ids, sess.Id) } return ids, nil } func teamMember(members []api.TeamMember, name string) (api.TeamMember, error) { for _, mem := range members { if mem.Name == name { return mem, nil } } return api.TeamMember{}, fmt.Errorf("the stage's frozen workflow has no agent %q", name) } func (m *Manager) discardStages(ctx context.Context, tasks []api.Task, keep map[string]bool) error { for _, t := range tasks { if keep[t.Id] || t.Status != api.TaskStatusActive { continue } _, err := m.Discard(ctx, t, "byre") if errors.Is(err, ErrConflict) { slog.Info("stage task not discarded", "task", t.Id, "err", err) continue } if err != nil { return fmt.Errorf("discard stage task %s: %w", t.Id, err) } } return nil } func (m *Manager) workflowStatus(ctx context.Context, task api.Task) (string, error) { finished, err := m.store.WorkflowFinished(ctx, *task.WorkflowRun) if err != nil { return "", err } output, _, err := m.store.StageResult(ctx, task.Id) if err != nil { return "", err } status := fmt.Sprintf("stage %q", *task.WorkflowStage) if output != "" { status += " (finished)" } if finished { status += ", workflow finished" } return status, nil } func (m *Manager) finishStageTool(l *live) mcp.Tool { return mcp.Tool{ Name: "finish_stage", Description: "Finish this task's stage of its workflow. Call it once the stage's work is done; when this turn ends, byre hands the output to the stages that come after this one, " + "each of which starts in its own task once every stage it comes after has finished (once every stage has finished, the workflow is finished). Calling it again in the same turn replaces the output. " + "The call counts only for the turn that makes it: if that turn ends in an error or is interrupted, it is discarded, and you call it again once the stage's work is done.", InputSchema: json.RawMessage(`{ "type": "object", "properties": { "output": {"type": "string", "description": "What the stages after this one start from: everything they need from this stage, since they see nothing else of your context. For a last stage, what this branch of the workflow produced."} }, "required": ["output"] }`), Handler: func(ctx context.Context, raw json.RawMessage) (string, error) { args := struct { Output string `json:"output"` }{} err := json.Unmarshal(raw, &args) if err != nil { return "", fmt.Errorf("malformed finish_stage call: %w", err) } if strings.TrimSpace(args.Output) == "" { return "", fmt.Errorf("%w: output is required", ErrInvalid) } recorded, _, err := m.store.StageResult(ctx, l.taskID) if err != nil { return "", err } if recorded != "" { return "", fmt.Errorf("%w: stage %q already finished; its output went to the stages after it", ErrConflict, l.stage) } frozen, err := m.store.TaskWorkflow(ctx, l.taskID) if err != nil { return "", err } err = m.store.SetStageOutput(ctx, l.id, args.Output) if err != nil { return "", err } next := workflow.Next(frozen.Definition, l.stage) if len(next) == 0 { return lastStageText(frozen.Definition) + " End your turn now with your summary.", nil } return fmt.Sprintf("When this turn ends, byre hands the output to %s. End your turn now with your summary.", quoteList(next)), nil }, } } func describeTasks(tasks []api.Task) string { parts := []string{} for _, t := range tasks { parts = append(parts, fmt.Sprintf("task %s %q from %s", t.Id, t.Title, t.BaseRef)) } return strings.Join(parts, ", ") } func quoteList(names []string) string { quoted := []string{} for _, n := range names { quoted = append(quoted, fmt.Sprintf("%q", n)) } return strings.Join(quoted, ", ") } func lastStageText(def api.WorkflowDefinition) string { ends := workflow.Ends(def) if len(ends) == 1 { return "This is the workflow's last stage: the workflow finishes when this turn ends, and byre discards its other stage tasks." } return fmt.Sprintf("This is one of the workflow's last stages (%s): the workflow finishes once all of them have, and byre then discards its other stage tasks, keeping each last stage's task.", quoteList(ends)) } func stageGuidance(frozen store.FrozenWorkflow, stage string) string { next := workflow.Next(frozen.Definition, stage) then := strings.TrimSuffix(lastStageText(frozen.Definition), ".") if len(next) > 0 { then = fmt.Sprintf("byre then hands the output to %s, each in its own task, which gets the output and nothing else of your context", quoteList(next)) } return fmt.Sprintf("\n- This task runs stage %q of byre workflow %q. When this stage's work is done, call byre's finish_stage tool with its output, then end your turn: "+ "%s. Until you call it, the workflow waits on this stage.", stage, frozen.Name, then) } const workflowHelp = "The definition's fields are the read_knowledge article workflow-definitions." const workflowDefinitionSchema = `{ "type": "object", "properties": { "description": {"type": "string"}, "steps": {"type": "array", "items": {"$ref": "#/$defs/step"}} }, "required": ["steps"], "additionalProperties": false }` const workflowSchemaDefs = `"$defs": { "step": { "type": "object", "description": "Exactly one of stage (with model or agents, and prompt), order or parallel.", "properties": { "stage": {"type": "string"}, "model": {"type": "string"}, "prompt": {"type": "string"}, "agents": {"type": "array", "items": {"$ref": "#/$defs/agent"}}, "order": {"type": "array", "items": {"$ref": "#/$defs/step"}}, "parallel": {"type": "array", "items": {"$ref": "#/$defs/step"}} }, "additionalProperties": false }, "agent": { "type": "object", "properties": { "name": {"type": "string"}, "model": {"type": "string"}, "prompt": {"type": "string"} }, "required": ["name", "model", "prompt"], "additionalProperties": false } }` func formatDefinition(def api.WorkflowDefinition) (string, error) { b, err := json.MarshalIndent(def, "", " ") return string(b), err } func (m *Manager) workflowTools(tool func(name, description, schema string, run func(ctx context.Context, user api.User, raw json.RawMessage) (string, error)) mcp.Tool) []mcp.Tool { return []mcp.Tool{ tool("list_workflows", "List the workflows on the server: each one's name and description.", `{"type": "object", "properties": {}}`, func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) { workflows, err := m.store.ListWorkflows(ctx) if err != nil { return "", err } if len(workflows) == 0 { return "No workflows.", nil } b := &strings.Builder{} for _, w := range workflows { fmt.Fprintf(b, "- %q", w.Name) if w.Description != "" { fmt.Fprintf(b, ": %s", w.Description) } b.WriteString("\n") } return b.String(), nil }), tool("read_workflow", "Read a workflow's definition: with name, the workflow on the server; with task_id, the copy that task froze when it was created. "+workflowHelp, `{"type": "object", "properties": { "name": {"type": "string"}, "task_id": {"type": "string"} }}`, func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) { args := struct { Name string `json:"name"` TaskID string `json:"task_id"` }{} err := json.Unmarshal(raw, &args) if err != nil { return "", err } if (args.Name == "") == (args.TaskID == "") { return "", fmt.Errorf("%w: give exactly one of name and task_id", ErrInvalid) } if args.TaskID != "" { frozen, err := m.store.TaskWorkflow(ctx, args.TaskID) if err != nil { return "", err } text, err := formatDefinition(frozen.Definition) if err != nil { return "", err } return fmt.Sprintf("Task %s froze workflow %q:\n\n%s", args.TaskID, frozen.Name, text), nil } def, err := m.store.GetWorkflow(ctx, args.Name) if err != nil { return "", err } return formatDefinition(def) }), tool("save_workflow", "Create or replace a workflow on the server from its definition. Tasks that already froze it keep their copy. "+ "The name is one line of 1 to 100 characters, shown to the human on buttons and pickers. "+workflowHelp, `{"type": "object", "properties": { "name": {"type": "string"}, "definition": `+workflowDefinitionSchema+` }, "required": ["name", "definition"], `+workflowSchemaDefs+`}`, func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) { args := struct { Name string `json:"name"` Definition json.RawMessage `json:"definition"` }{} err := json.Unmarshal(raw, &args) if err != nil { return "", err } w, err := m.SaveWorkflow(ctx, args.Name, "", args.Definition, leadActor(user.Login)) if err != nil { return "", err } return fmt.Sprintf("Saved workflow %q.", w.Name), nil }), tool("set_review_workflow", "Set the review workflow (any saved workflow whose stages are single agents, not teams) whose stages review every snapshot of a project's tasks, from the next snapshot on; an empty workflow turns reviews off for the project. Only when the human asks.", `{"type": "object", "properties": { "project_id": {"type": "string"}, "workflow": {"type": "string"} }, "required": ["project_id", "workflow"]}`, func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) { args := struct { ProjectID string `json:"project_id"` Workflow string `json:"workflow"` }{} err := json.Unmarshal(raw, &args) if err != nil { return "", err } p, err := m.SetReviewWorkflow(ctx, args.ProjectID, args.Workflow, leadActor(user.Login)) if err != nil { return "", err } if p.ReviewWorkflow == "" { return fmt.Sprintf("Project %s has no review workflow now: its snapshots aren't reviewed.", p.Name), nil } return fmt.Sprintf("Project %s reviews with workflow %q from its next snapshot on.", p.Name, p.ReviewWorkflow), nil }), tool("set_audit_workflow", "Set the audit workflow (any saved workflow) that byre runs in the background on a project's default branch: one run at a time, started whenever the branch's newest commit hasn't had one, to check the whole codebase against its standards rather than one change. "+ "An audit's tasks stay out of list_tasks and your signals until one asks a question, calls ready_for_merge or hits an error; one that suggests discarding first is discarded by byre. An empty workflow turns audits off. Only when the human asks.", `{"type": "object", "properties": { "project_id": {"type": "string"}, "workflow": {"type": "string"} }, "required": ["project_id", "workflow"]}`, func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) { args := struct { ProjectID string `json:"project_id"` Workflow string `json:"workflow"` }{} err := json.Unmarshal(raw, &args) if err != nil { return "", err } p, err := m.SetAuditWorkflow(ctx, args.ProjectID, args.Workflow, leadActor(user.Login)) if err != nil { return "", err } if p.AuditWorkflow == "" { return fmt.Sprintf("Project %s has no audit workflow now: it isn't audited.", p.Name), nil } return fmt.Sprintf("Project %s is audited with workflow %q.", p.Name, p.AuditWorkflow), nil }), tool("delete_workflow", "Delete a workflow from the server. Tasks that froze it keep their copy. Only when the human asks.", `{"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}`, func(ctx context.Context, user api.User, raw json.RawMessage) (string, error) { args := struct { Name string `json:"name"` }{} err := json.Unmarshal(raw, &args) if err != nil { return "", err } err = m.store.DeleteWorkflow(ctx, args.Name, leadActor(user.Login)) if err != nil { return "", err } return fmt.Sprintf("Deleted workflow %q.", args.Name), nil }), } }