diff --git a/api/openapi.yaml b/api/openapi.yaml index ba54475..89fde0d 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -351,7 +351,7 @@ paths: operationId: createTask description: > Creates the task's branch and worktree from the project's default branch, and with prompt starts its agent, or - with workflow, starts a run of that workflow from it. + with workflow, creates a run of that workflow, whose stage tasks start from it. requestBody: required: true content: @@ -422,8 +422,8 @@ paths: get: operationId: getTaskWorkflow description: > - The workflow the task froze when it was created, as it was then. Later edits and deletes of the workflow - don't change it. + The workflow the task's run froze when it was created, as it was then, for a workflow run's task or one of + its stage tasks. Later edits and deletes of the workflow don't change it. responses: "200": description: The frozen workflow. @@ -455,8 +455,9 @@ paths: get: operationId: getWorkflowRun description: > - The progress of the workflow run the task belongs to: the definition the task froze, and for each stage its - state and every task that attempted it, with the attempts whose outputs each one started from. + The progress of the workflow run whose task this is: the definition it froze, and for each stage its state + and every task that attempted it, with the attempts whose outputs each one started from. 404 for a task that + isn't a run's. responses: "200": description: The run's progress. @@ -1477,19 +1478,18 @@ components: forked_from: { type: string, description: "The task this one was forked from; absent for other tasks." } forked_at: { type: integer, description: "With forked_from: the n of the snapshot it was forked at." } merge_queued_at: { type: string, format: date-time, description: "When Merge was requested while another task in the project was merging; absent once the merge starts or when none is held." } - workflow: { type: string, description: "The name of the workflow the task froze at creation (getTaskWorkflow has its copy); absent for a task without one." } - workflow_run: { type: string, description: "With workflow: the id every task of one run of the workflow shares." } - workflow_stage: { type: string, description: "With workflow: the name of the stage this task runs." } - audit: { type: string, description: "The default branch commit this task audits, for a task of a run of its project's audit workflow; absent for other tasks." } - background: { type: boolean, description: "Whether the task is an audit that hasn't asked for attention yet: no question, ready_for_merge or error so far. The app keeps it out of the task switcher and shows it muted on the Project tab, and the lead gets no signals about it." } + workflow: { type: string, description: "For a workflow run's task: the name of the workflow it froze at creation (getTaskWorkflow has its copy). A run's task has no agent, worktree or branch (branch, worktree_path and base_ref are empty); its stage tasks are its subtasks, and it closes once they all have, as merged when any of them merged. Absent for other tasks." } + workflow_stage: { type: string, description: "For a stage task of a workflow run: the name of the stage it runs; parent_task_id is the run's task. Absent for other tasks." } + audit: { type: string, description: "The default branch commit an audit audits, on the task of a run of its project's audit workflow; absent for other tasks." } + background: { type: boolean, description: "Whether the task is an audit's run or stage task that hasn't asked for attention yet: no question, ready_for_merge or error so far (for a run, from any of its stage tasks). The app keeps it out of the task switcher and shows it muted on the Project tab, and the lead gets no signals about it." } state: type: string - description: "The one state the task is in, kept by the store in the same transaction as every change to it (a task.state event carries each change). The first that holds: merged or discarded once closed; discarding; asking while it has an open question; merging while a Merge or a merge into its parent is under way; merge_held while its Merge waits for another merge in the project; merge_after_review while a Merge with after_review waits for its review; running while one of its agents is mid-turn or has subagents running; reviewing while a review runs; review_pending between a turn's end and its snapshot's review starting or being skipped; ready_to_merge or discard_suggested when the agent's latest ready_for_merge or suggest_discard still stands; finished when it runs a workflow stage whose finish_stage has taken; idle; no_agent when it has no live agent." + description: "The one state the task is in, kept by the store in the same transaction as every change to it (a task.state event carries each change). The first that holds: merged or discarded once closed; for a workflow run's task, finished once the run finished, running while one of its stage tasks is running, else idle; discarding; asking while it has an open question; merging while a Merge or a merge into its parent is under way; merge_held while its Merge waits for another merge in the project; merge_after_review while a Merge with after_review waits for its review; running while one of its agents is mid-turn or has subagents running; reviewing while a review runs; review_pending between a turn's end and its snapshot's review starting or being skipped; ready_to_merge or discard_suggested when the agent's latest ready_for_merge or suggest_discard still stands; finished when it runs a workflow stage whose finish_stage has taken; idle; no_agent when it has no live agent." enum: [running, idle, finished, review_pending, reviewing, merging, discarding, merged, discarded, no_agent, asking, merge_held, merge_after_review, ready_to_merge, discard_suggested] x-enum-varnames: [TaskStateRunning, TaskStateIdle, TaskStateFinished, TaskStateReviewPending, TaskStateReviewing, TaskStateMerging, TaskStateDiscarding, TaskStateMerged, TaskStateDiscarded, TaskStateNoAgent, TaskStateAsking, TaskStateMergeHeld, TaskStateMergeAfterReview, TaskStateReadyToMerge, TaskStateDiscardSuggested] merging: { type: boolean, description: "Whether a Merge was started and hasn't finished: its turn is running, or the turn ended and byre is squashing, pushing and closing the task." } discarding: { type: boolean, description: "Whether a Discard was started and the task isn't closed yet: its summary turn is waiting for the running turn, running, or done and the task is being removed." } - parent_task_id: { type: string, description: "The task whose agent created this one as its subtask; absent for other tasks." } + parent_task_id: { type: string, description: "The task whose agent created this one as its subtask, or for a stage task its workflow run's task; absent for other tasks." } merging_into_parent: { type: boolean, description: "Whether a merge of this subtask into its parent was requested and the parent hasn't merged it yet." } merge_after_review: { type: boolean, description: "Whether a Merge with after_review is waiting for the running review of the latest snapshot to finish." } summary_seq: { type: integer, format: int64, description: "The seq of the task's latest turn.ended event with summary: true; 0 when it has none." } @@ -1557,9 +1557,9 @@ components: workflow: type: string description: > - The name of a saved workflow to run instead of creating one task with no agent: each stage that starts it - becomes its own task, titled ": <stage>", with the title as the run's brief. The response is the - first of those tasks. + The name of a saved workflow to run instead of creating one task with no agent: the run gets a task of its + own, titled with the title, and each stage that starts it becomes its subtask, titled "<title>: <stage>", + with the title as the run's brief. The response is the run's task. WorkflowSummary: type: object @@ -1624,10 +1624,9 @@ components: WorkflowRun: type: object - required: [name, run, title, brief, definition, finished, stages] + required: [name, title, brief, definition, finished, stages] properties: name: { type: string } - run: { type: string } title: { type: string } brief: { type: string } definition: { $ref: "#/components/schemas/WorkflowDefinition" } diff --git a/internal/agents/afterreview.go b/internal/agents/afterreview.go index 76a976a..278a16b 100644 --- a/internal/agents/afterreview.go +++ b/internal/agents/afterreview.go @@ -21,7 +21,7 @@ func (m *Manager) MergeAfterReview(ctx context.Context, task api.Task, user api. if err != nil { return api.Task{}, err } - if target == api.MergeTargetParent && task.ParentTaskId == nil { + if target == api.MergeTargetParent && (task.ParentTaskId == nil || task.WorkflowStage != nil) { return api.Task{}, fmt.Errorf("%w: task %s is not a subtask", ErrInvalid, task.Id) } if target == api.MergeTargetMain { diff --git a/internal/agents/audit.go b/internal/agents/audit.go index 00510cd..81e7757 100644 --- a/internal/agents/audit.go +++ b/internal/agents/audit.go @@ -94,6 +94,9 @@ func (m *Manager) startAudit(ctx context.Context, projectID string) error { return nil } for _, t := range tasks { + if t.WorkflowStage == nil { + continue + } 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 @@ -126,7 +129,7 @@ func (m *Manager) dismissAudit(taskID string) { slog.Error("dismiss audit", "task", taskID, "err", err) return } - if !task.Background || task.Status != api.TaskStatusActive { + if !task.Background || task.Status != api.TaskStatusActive || task.WorkflowStage == nil { return } output, _, err := m.store.StageResult(ctx, taskID) diff --git a/internal/agents/fork.go b/internal/agents/fork.go index a613d58..769be2d 100644 --- a/internal/agents/fork.go +++ b/internal/agents/fork.go @@ -12,7 +12,6 @@ import ( "g.fc.run/ai/byre/internal/api" "g.fc.run/ai/byre/internal/ids" "g.fc.run/ai/byre/internal/mcp" - "g.fc.run/ai/byre/internal/store" ) func (m *Manager) Fork(ctx context.Context, src api.Task, project api.Project, n int, creator api.User) (api.Task, error) { @@ -173,15 +172,6 @@ func (m *Manager) fork(ctx context.Context, src api.Task, project api.Project, n return api.Task{}, "", fmt.Errorf("%w: snapshot %d of task %s has no recorded agent message at or before it to fork the conversation from", ErrConflict, n, src.Id) } - var frozen *store.FrozenWorkflow - if src.Workflow != nil { - f, err := m.store.TaskWorkflow(ctx, src.Id) - if err != nil { - return api.Task{}, "", err - } - frozen = &f - } - t, err := m.createTask(ctx, project, api.Task{ Title: title, MergeTitle: src.MergeTitle, @@ -189,10 +179,9 @@ func (m *Manager) fork(ctx context.Context, src api.Task, project api.Project, n TodoItem: src.TodoItem, ForkedFrom: &src.Id, ForkedAt: &n, - WorkflowRun: src.WorkflowRun, WorkflowStage: src.WorkflowStage, ParentTaskId: src.ParentTaskId, - }, frozen, snap.CommitSha, creator.Login) + }, snap.CommitSha, creator.Login) if err != nil { return api.Task{}, "", err } diff --git a/internal/agents/guidance.go b/internal/agents/guidance.go index 35bb15a..e0b8f87 100644 --- a/internal/agents/guidance.go +++ b/internal/agents/guidance.go @@ -110,7 +110,7 @@ func (m *Manager) taskGuidance(ctx context.Context, task api.Task) (string, erro g += fmt.Sprintf("\n- This task was started from the item %q in %s at the repo root. When the work is done, remove that item from %s in this worktree, so the removal lands in the same merge as the work.", *task.TodoItem, todo.File, todo.File) } - if task.ParentTaskId != nil { + if task.ParentTaskId != nil && task.WorkflowStage == nil { parent, err := m.store.GetTask(ctx, *task.ParentTaskId) if err != nil { return "", err diff --git a/internal/agents/kb/workflow-definitions.md b/internal/agents/kb/workflow-definitions.md index 10b601d..7bf8124 100644 --- a/internal/agents/kb/workflow-definitions.md +++ b/internal/agents/kb/workflow-definitions.md @@ -20,7 +20,7 @@ A stage gets the outputs of the stages that end the step before it: the last sta | `prompt` | The stage agent's instructions. | | `agents` | In place of `model`: a list of at least two, each with `name`, `model` and `prompt`, that runs the stage as a team. They work in the stage's one task and worktree and talk through a shared board; each gets the stage's `prompt` and then its own, and the first agent finishes the stage. | -Where a stage's task branches from follows from the shape. A stage after exactly one stage starts from the commit that stage finished at. A stage after a `parallel` starts from the default branch and is told the branches it can merge. A stage with nothing before it starts where the workflow was started. A workflow is finished once every stage has finished; when it ends with a `parallel`, each branch's last stage keeps its task to merge, and the run's other stage tasks are discarded. A review is delivered to the task's agent, every stage's findings in one message, once every stage has reviewed. An `order` or `parallel` can't be empty. +Where a stage's task branches from follows from the shape. A stage after exactly one stage starts from the commit that stage finished at. A stage after a `parallel` starts from the default branch and is told the branches it can merge. A stage with nothing before it starts where the workflow was started. A run on a brief has a task of its own, with no agent, worktree or branch, and its stage tasks are that task's subtasks; it closes once they all have. A workflow is finished once every stage has finished; when it ends with a `parallel`, each branch's last stage keeps its task to merge, and the run's other stage tasks are discarded. A review is delivered to the task's agent, every stage's findings in one message, once every stage has reviewed. An `order` or `parallel` can't be empty. ```json { diff --git a/internal/agents/lead.go b/internal/agents/lead.go index 43696a7..1f03301 100644 --- a/internal/agents/lead.go +++ b/internal/agents/lead.go @@ -580,6 +580,9 @@ func (m *Manager) signalText(ctx context.Context, ev api.Event) (string, error) if task.ForkedFrom != nil { return fmt.Sprintf("%s was forked by %s from %s at snapshot %d.", m.taskLabel(ctx, task.Id), ev.Actor, m.taskLabel(ctx, *task.ForkedFrom), *task.ForkedAt), nil } + if task.WorkflowStage != nil { + return fmt.Sprintf("%s was created to run stage %q of %s.", m.taskLabel(ctx, task.Id), *task.WorkflowStage, m.taskLabel(ctx, *task.ParentTaskId)), nil + } if task.ParentTaskId != nil { return fmt.Sprintf("%s was created as a subtask of %s by its agent.", m.taskLabel(ctx, task.Id), m.taskLabel(ctx, *task.ParentTaskId)), nil } diff --git a/internal/agents/leadtools.go b/internal/agents/leadtools.go index 560e624..9d29909 100644 --- a/internal/agents/leadtools.go +++ b/internal/agents/leadtools.go @@ -927,14 +927,18 @@ func (m *Manager) listTasksText(ctx context.Context) (string, error) { open++ } } - fmt.Fprintf(b, "- %s %q: %s; %d open questions; worktree %s", t.Id, t.Title, agent, open, t.WorktreePath) + if t.Workflow != nil { + fmt.Fprintf(b, "- %s %q: %s", t.Id, t.Title, agent) + } else { + fmt.Fprintf(b, "- %s %q: %s; %d open questions; worktree %s", t.Id, t.Title, agent, open, t.WorktreePath) + } if t.TodoItem != nil { fmt.Fprintf(b, "; from TODO item %q", *t.TodoItem) } if t.ForkedFrom != nil { fmt.Fprintf(b, "; forked from %s at snapshot %d", *t.ForkedFrom, *t.ForkedAt) } - if t.ParentTaskId != nil { + if t.ParentTaskId != nil && t.WorkflowStage == nil { fmt.Fprintf(b, "; subtask of %s", *t.ParentTaskId) if t.MergingIntoParent { b.WriteString(", merging into it") @@ -946,12 +950,12 @@ func (m *Manager) listTasksText(ctx context.Context) (string, error) { if t.MergeAfterReview { b.WriteString("; merges once the running review finishes with no findings") } - if t.Workflow != nil { - stage, err := m.workflowStatus(ctx, t) + if t.Workflow != nil || t.WorkflowStage != nil { + status, err := m.workflowStatus(ctx, t) if err != nil { return "", err } - fmt.Fprintf(b, "; workflow %q, %s", *t.Workflow, stage) + fmt.Fprintf(b, "; %s", status) } b.WriteString("\n") } diff --git a/internal/agents/manager.go b/internal/agents/manager.go index 2217129..038661a 100644 --- a/internal/agents/manager.go +++ b/internal/agents/manager.go @@ -223,6 +223,9 @@ func (m *Manager) create(ctx context.Context, task api.Task, creator api.User, m } func (m *Manager) newSession(ctx context.Context, task api.Task, creator api.User, member, model string) (api.AgentSession, error) { + if task.Workflow != nil { + return api.AgentSession{}, fmt.Errorf("%w: task %s is a workflow run, which has no agent; its stage tasks have theirs", ErrInvalid, task.Id) + } existing, err := m.store.ListSessions(ctx, task.Id) if err != nil { return api.AgentSession{}, err diff --git a/internal/agents/ready.go b/internal/agents/ready.go index d07c782..0359aa0 100644 --- a/internal/agents/ready.go +++ b/internal/agents/ready.go @@ -182,7 +182,7 @@ func (m *Manager) tryPromptReady(ctx context.Context, taskID string, fromReview if output != "" && len(workflow.Next(frozen.Definition, *task.WorkflowStage)) > 0 { return nil } - finished, err := m.store.WorkflowFinished(ctx, *task.WorkflowRun) + finished, err := m.store.WorkflowFinished(ctx, *task.ParentTaskId) if err != nil { return err } diff --git a/internal/agents/subtasks.go b/internal/agents/subtasks.go index f9ac3a0..ee20850 100644 --- a/internal/agents/subtasks.go +++ b/internal/agents/subtasks.go @@ -69,7 +69,7 @@ func (m *Manager) subtaskTools(l *live) []mcp.Tool { if err != nil { return "", err } - task, err := m.createTask(ctx, project, api.Task{Title: args.Title, CreatedBy: creator.Login, ParentTaskId: &l.taskID}, nil, base, creator.Login) + task, err := m.createTask(ctx, project, api.Task{Title: args.Title, CreatedBy: creator.Login, ParentTaskId: &l.taskID}, base, creator.Login) if err != nil { return "", err } @@ -302,7 +302,7 @@ func (m *Manager) messageTask(ctx context.Context, task api.Task, user api.User, } func (m *Manager) MergeIntoParent(ctx context.Context, task api.Task, user api.User) (api.Task, error) { - if task.ParentTaskId == nil { + if task.ParentTaskId == nil || task.WorkflowStage != nil { return api.Task{}, fmt.Errorf("%w: task %s is not a subtask", ErrInvalid, task.Id) } err := m.checkNoSubtasks(ctx, task) @@ -411,7 +411,7 @@ func (m *Manager) reportToParent(taskID, result string) { slog.Error("load subtask to report", "task", taskID, "err", err) return } - if task.ParentTaskId == nil || task.MergingIntoParent { + if task.ParentTaskId == nil || task.WorkflowStage != nil || task.MergingIntoParent { return } parent, err := m.store.GetTask(ctx, *task.ParentTaskId) diff --git a/internal/agents/tasks.go b/internal/agents/tasks.go index c37055d..4035525 100644 --- a/internal/agents/tasks.go +++ b/internal/agents/tasks.go @@ -16,7 +16,7 @@ import ( ) func (m *Manager) CreateTask(ctx context.Context, project api.Project, title, createdBy, base, actor string) (api.Task, error) { - return m.createTask(ctx, project, api.Task{Title: title, CreatedBy: createdBy}, nil, base, actor) + return m.createTask(ctx, project, api.Task{Title: title, CreatedBy: createdBy}, base, actor) } func (m *Manager) StartTask(ctx context.Context, project api.Project, creator api.User, title, prompt, model, client string, images []string) (api.Task, error) { @@ -81,7 +81,7 @@ func (m *Manager) StartTodo(ctx context.Context, project api.Project, creator ap return nil, err } } else { - task, err := m.createTask(ctx, project, api.Task{Title: item.Title, CreatedBy: creator.Login, TodoItem: &item.Title}, nil, project.DefaultBranch, actor) + task, err := m.createTask(ctx, project, api.Task{Title: item.Title, CreatedBy: creator.Login, TodoItem: &item.Title}, project.DefaultBranch, actor) if err != nil { return nil, err } @@ -108,7 +108,7 @@ func todoPrompt(item api.TodoItem, discuss bool) string { const discussPrompt = "The human wants to discuss this item with you before any of it is built, to judge whether it should be built at all and in what shape. Zoom out before going into detail. Read what you need to understand it, then give your view: what problem it solves and whether that is worth solving, whether we should do it, what the alternatives are (another approach, a smaller change, or nothing), and how you would approach it, with the trade-offs and doubts you see. Talk it over in your final messages, where the human replies in the chat, and use ask_human as often as it helps. Each question belongs to that zoomed-out discussion, such as whether to do it, which alternative, or what shape, rather than the narrow details of getting it built, and later questions can build on earlier answers. Keep the conversation going turn by turn, and change nothing until they tell you to build it." -func (m *Manager) createTask(ctx context.Context, project api.Project, t api.Task, frozen *store.FrozenWorkflow, base, actor string) (api.Task, error) { +func (m *Manager) createTask(ctx context.Context, project api.Project, t api.Task, base, actor string) (api.Task, error) { t.Id = ids.New(ids.Task) t.ProjectId = project.Id if t.MergeTitle == "" { @@ -125,7 +125,7 @@ func (m *Manager) createTask(ctx context.Context, project api.Project, t api.Tas return api.Task{}, err } - err = m.store.CreateTask(ctx, t, frozen, actor) + err = m.store.CreateTask(ctx, t, nil, actor) if err != nil { rerr := git.RemoveWorktree(project.RepoPath, t.WorktreePath, t.Branch) if rerr != nil { diff --git a/internal/agents/workflows.go b/internal/agents/workflows.go index b9329d4..927f77b 100644 --- a/internal/agents/workflows.go +++ b/internal/agents/workflows.go @@ -7,6 +7,7 @@ import ( "fmt" "log/slog" "strings" + "time" "g.fc.run/ai/byre/internal/api" "g.fc.run/ai/byre/internal/git" @@ -68,24 +69,31 @@ func (m *Manager) TaskWorkflow(ctx context.Context, taskID string) (api.TaskWork } 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 task.Workflow == nil { + return api.WorkflowRun{}, fmt.Errorf("%w: task %s isn't a workflow run", store.ErrNotFound, taskID) + } + frozen, err := m.store.TaskWorkflow(ctx, taskID) if err != nil { return api.WorkflowRun{}, err } - events, err := m.store.RunEvents(ctx, *task.WorkflowRun) + tasks, err := m.store.RunTasks(ctx, taskID) + if err != nil { + return api.WorkflowRun{}, err + } + events, err := m.store.RunEvents(ctx, taskID) + if err != nil { + return api.WorkflowRun{}, err + } + finished, err := m.store.WorkflowFinished(ctx, taskID) 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{}} + run := api.WorkflowRun{Name: frozen.Name, Title: frozen.Title, Brief: frozen.Brief, Definition: frozen.Definition, Finished: finished, Stages: []api.WorkflowRunStage{}} after := map[string][]string{} discardedBy := map[string]string{} for _, ev := range events { @@ -100,8 +108,6 @@ func (m *Manager) WorkflowRun(ctx context.Context, taskID string) (api.WorkflowR after[*ev.TaskId] = ids case "task.discarded": discardedBy[*ev.TaskId] = ev.Actor - case "task.workflow_finished": - run.Finished = true } } @@ -232,33 +238,60 @@ func (m *Manager) StartWorkflow(ctx context.Context, project api.Project, creato frozen.Title = title frozen.Audit = audit - run := ids.New(ids.WorkflowRun) - tasks := []api.Task{} + run, err := m.createRun(ctx, project, creator, frozen, todoItem, actor) + if err != nil { + return nil, err + } + tasks := []api.Task{run} for _, stage := range workflow.Starts(frozen.Definition) { - task, err := m.startStage(ctx, project, creator, frozen, run, stage, base, todoItem, nil, actor) + task, err := m.startStage(ctx, project, creator, frozen, run, stage, base, 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) + if err == nil { + continue } + if len(tasks) == 1 { + _, derr := m.store.DiscardTask(ctx, run.Id, actor) + if derr != nil { + slog.Error("discard workflow run with no stage tasks", "task", run.Id, "err", derr) + } + } + 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) { +func (m *Manager) createRun(ctx context.Context, project api.Project, creator api.User, frozen store.FrozenWorkflow, todoItem *string, 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, + Id: ids.New(ids.Task), + ProjectId: project.Id, + Title: frozen.Title, + MergeTitle: frozen.Title, + Status: api.TaskStatusActive, + CreatedBy: creator.Login, + CreatedAt: time.Now().UTC(), + TodoItem: todoItem, } if frozen.Audit != "" { t.Audit = &frozen.Audit } - task, err := m.createTask(ctx, project, t, &frozen, base, actor) + err := m.store.CreateTask(ctx, t, &frozen, actor) + if err != nil { + return api.Task{}, err + } + return m.store.GetTask(ctx, t.Id) +} + +func (m *Manager) startStage(ctx context.Context, project api.Project, creator api.User, frozen store.FrozenWorkflow, run api.Task, stage, base 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, + WorkflowStage: &stage, + ParentTaskId: &run.Id, + } + task, err := m.createTask(ctx, project, t, base, actor) if err != nil { return api.Task{}, err } @@ -280,7 +313,7 @@ func (m *Manager) beginStage(ctx context.Context, task api.Task, project api.Pro 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}, + Payload: map[string]any{"id": task.Id, "workflow": frozen.Name, "run": *task.ParentTaskId, "stage": stage, "after": after}, }) if err != nil { return api.AgentSession{}, err @@ -401,16 +434,20 @@ func (m *Manager) nextStage(ctx context.Context, sessionID string) ([]string, er m.workflowMu.Lock() defer m.workflowMu.Unlock() + run, err := m.store.GetTask(ctx, *task.ParentTaskId) + if err != nil { + return nil, err + } next := workflow.Next(frozen.Definition, *task.WorkflowStage) if len(next) == 0 { - finished, err := m.store.WorkflowFinished(ctx, *task.WorkflowRun) + finished, err := m.store.WorkflowFinished(ctx, run.Id) if err != nil { return nil, err } if finished { return nil, m.store.ClearStage(ctx, sess.Id) } - tasks, err := m.store.RunTasks(ctx, *task.WorkflowRun) + tasks, err := m.store.RunTasks(ctx, run.Id) if err != nil { return nil, err } @@ -436,12 +473,12 @@ func (m *Manager) nextStage(ctx context.Context, sessionID string) ([]string, er if err != nil { return nil, err } - return nil, m.store.FinishWorkflow(ctx, task, sess.Id, frozen.Name, output, kept) + return nil, m.store.FinishWorkflow(ctx, run, sess.Id, frozen.Name, output, kept) } started := []string{} for _, n := range next { - ids, err := m.advanceTo(ctx, task, frozen, n) + ids, err := m.advanceTo(ctx, run, frozen, n) if err != nil { return started, err } @@ -450,8 +487,8 @@ func (m *Manager) nextStage(ctx context.Context, sessionID string) ([]string, er 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) +func (m *Manager) advanceTo(ctx context.Context, run api.Task, frozen store.FrozenWorkflow, stage string) ([]string, error) { + tasks, err := m.store.RunTasks(ctx, run.Id) if err != nil { return nil, err } @@ -463,11 +500,11 @@ func (m *Manager) advanceTo(ctx context.Context, from api.Task, frozen store.Fro } parents = append(parents, p) } - project, err := m.store.GetProject(ctx, from.ProjectId) + project, err := m.store.GetProject(ctx, run.ProjectId) if err != nil { return nil, err } - creator, err := m.store.GetUser(ctx, from.CreatedBy) + creator, err := m.store.GetUser(ctx, run.CreatedBy) if err != nil { return nil, err } @@ -488,7 +525,7 @@ func (m *Manager) advanceTo(ctx context.Context, from api.Task, frozen store.Fro default: base = project.DefaultBranch } - task, err := m.startStage(ctx, project, creator, frozen, *from.WorkflowRun, stage, base, from.TodoItem, parents, "byre") + task, err := m.startStage(ctx, project, creator, frozen, run, stage, base, parents, "byre") if err != nil { return nil, err } @@ -604,21 +641,25 @@ func (m *Manager) discardStages(ctx context.Context, tasks []api.Task, keep map[ } 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 + if task.Workflow != nil { + finished, err := m.store.WorkflowFinished(ctx, task.Id) + if err != nil { + return "", err + } + status := fmt.Sprintf("run of workflow %q, with no agent, worktree or branch; its stage tasks are its subtasks", *task.Workflow) + if finished { + status += "; workflow finished" + } + return status, nil } output, _, err := m.store.StageResult(ctx, task.Id) if err != nil { return "", err } - status := fmt.Sprintf("stage %q", *task.WorkflowStage) + status := fmt.Sprintf("stage %q of workflow run %s", *task.WorkflowStage, *task.ParentTaskId) if output != "" { status += " (finished)" } - if finished { - status += ", workflow finished" - } return status, nil } @@ -673,6 +714,10 @@ func (m *Manager) finishStageTool(l *live) mcp.Tool { func describeTasks(tasks []api.Task) string { parts := []string{} for _, t := range tasks { + if t.Workflow != nil { + parts = append(parts, fmt.Sprintf("run task %s %q", t.Id, t.Title)) + continue + } parts = append(parts, fmt.Sprintf("task %s %q from %s", t.Id, t.Title, t.BaseRef)) } return strings.Join(parts, ", ") diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 063cb96..ff43872 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -1307,10 +1307,10 @@ type Tailnet struct { // Task defines model for Task. type Task struct { - // Audit The default branch commit this task audits, for a task of a run of its project's audit workflow; absent for other tasks. + // Audit The default branch commit an audit audits, on the task of a run of its project's audit workflow; absent for other tasks. Audit *string `json:"audit,omitempty"` - // Background Whether the task is an audit that hasn't asked for attention yet: no question, ready_for_merge or error so far. The app keeps it out of the task switcher and shows it muted on the Project tab, and the lead gets no signals about it. + // Background Whether the task is an audit's run or stage task that hasn't asked for attention yet: no question, ready_for_merge or error so far (for a run, from any of its stage tasks). The app keeps it out of the task switcher and shows it muted on the Project tab, and the lead gets no signals about it. Background bool `json:"background"` BaseRef string `json:"base_ref"` Branch string `json:"branch"` @@ -1351,14 +1351,14 @@ type Task struct { // MergingIntoParent Whether a merge of this subtask into its parent was requested and the parent hasn't merged it yet. MergingIntoParent bool `json:"merging_into_parent"` - // ParentTaskId The task whose agent created this one as its subtask; absent for other tasks. + // ParentTaskId The task whose agent created this one as its subtask, or for a stage task its workflow run's task; absent for other tasks. ParentTaskId *string `json:"parent_task_id,omitempty"` // PortBase The first of the task's block of ports (port_base to port_base+9), passed to its agent and terminal as PORT. PortBase int `json:"port_base"` ProjectId string `json:"project_id"` - // State The one state the task is in, kept by the store in the same transaction as every change to it (a task.state event carries each change). The first that holds: merged or discarded once closed; discarding; asking while it has an open question; merging while a Merge or a merge into its parent is under way; merge_held while its Merge waits for another merge in the project; merge_after_review while a Merge with after_review waits for its review; running while one of its agents is mid-turn or has subagents running; reviewing while a review runs; review_pending between a turn's end and its snapshot's review starting or being skipped; ready_to_merge or discard_suggested when the agent's latest ready_for_merge or suggest_discard still stands; finished when it runs a workflow stage whose finish_stage has taken; idle; no_agent when it has no live agent. + // State The one state the task is in, kept by the store in the same transaction as every change to it (a task.state event carries each change). The first that holds: merged or discarded once closed; for a workflow run's task, finished once the run finished, running while one of its stage tasks is running, else idle; discarding; asking while it has an open question; merging while a Merge or a merge into its parent is under way; merge_held while its Merge waits for another merge in the project; merge_after_review while a Merge with after_review waits for its review; running while one of its agents is mid-turn or has subagents running; reviewing while a review runs; review_pending between a turn's end and its snapshot's review starting or being skipped; ready_to_merge or discard_suggested when the agent's latest ready_for_merge or suggest_discard still stands; finished when it runs a workflow stage whose finish_stage has taken; idle; no_agent when it has no live agent. State TaskState `json:"state"` Status TaskStatus `json:"status"` @@ -1371,18 +1371,15 @@ type Task struct { // TodoItem The title of the TODO.md item the task was started from; absent for other tasks. TodoItem *string `json:"todo_item,omitempty"` - // Workflow The name of the workflow the task froze at creation (getTaskWorkflow has its copy); absent for a task without one. + // Workflow For a workflow run's task: the name of the workflow it froze at creation (getTaskWorkflow has its copy). A run's task has no agent, worktree or branch (branch, worktree_path and base_ref are empty); its stage tasks are its subtasks, and it closes once they all have, as merged when any of them merged. Absent for other tasks. Workflow *string `json:"workflow,omitempty"` - // WorkflowRun With workflow: the id every task of one run of the workflow shares. - WorkflowRun *string `json:"workflow_run,omitempty"` - - // WorkflowStage With workflow: the name of the stage this task runs. + // WorkflowStage For a stage task of a workflow run: the name of the stage it runs; parent_task_id is the run's task. Absent for other tasks. WorkflowStage *string `json:"workflow_stage,omitempty"` WorktreePath string `json:"worktree_path"` } -// TaskState The one state the task is in, kept by the store in the same transaction as every change to it (a task.state event carries each change). The first that holds: merged or discarded once closed; discarding; asking while it has an open question; merging while a Merge or a merge into its parent is under way; merge_held while its Merge waits for another merge in the project; merge_after_review while a Merge with after_review waits for its review; running while one of its agents is mid-turn or has subagents running; reviewing while a review runs; review_pending between a turn's end and its snapshot's review starting or being skipped; ready_to_merge or discard_suggested when the agent's latest ready_for_merge or suggest_discard still stands; finished when it runs a workflow stage whose finish_stage has taken; idle; no_agent when it has no live agent. +// TaskState The one state the task is in, kept by the store in the same transaction as every change to it (a task.state event carries each change). The first that holds: merged or discarded once closed; for a workflow run's task, finished once the run finished, running while one of its stage tasks is running, else idle; discarding; asking while it has an open question; merging while a Merge or a merge into its parent is under way; merge_held while its Merge waits for another merge in the project; merge_after_review while a Merge with after_review waits for its review; running while one of its agents is mid-turn or has subagents running; reviewing while a review runs; review_pending between a turn's end and its snapshot's review starting or being skipped; ready_to_merge or discard_suggested when the agent's latest ready_for_merge or suggest_discard still stands; finished when it runs a workflow stage whose finish_stage has taken; idle; no_agent when it has no live agent. type TaskState string // TaskStatus defines model for Task.Status. @@ -1412,7 +1409,7 @@ type TaskCreate struct { // Title The task's title. Without it the task starts untitled, and its agent names it with set_task_title. Required with workflow. Title *string `json:"title,omitempty"` - // Workflow The name of a saved workflow to run instead of creating one task with no agent: each stage that starts it becomes its own task, titled "<title>: <stage>", with the title as the run's brief. The response is the first of those tasks. + // Workflow The name of a saved workflow to run instead of creating one task with no agent: the run gets a task of its own, titled with the title, and each stage that starts it becomes its subtask, titled "<title>: <stage>", with the title as the run's brief. The response is the run's task. Workflow *string `json:"workflow,omitempty"` } @@ -1587,7 +1584,6 @@ type WorkflowRun struct { // Finished Whether the final stage finished, which ends the run. Finished bool `json:"finished"` Name string `json:"name"` - Run string `json:"run"` // Stages In the definition's order. Stages []WorkflowRunStage `json:"stages"` @@ -4475,318 +4471,320 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", - "h/81AA==", + "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", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/store/migrateruns.go b/internal/store/migrateruns.go new file mode 100644 index 0000000..41c813b --- /dev/null +++ b/internal/store/migrateruns.go @@ -0,0 +1,70 @@ +package store + +import ( + "database/sql" + "fmt" + + "g.fc.run/ai/byre/internal/ids" +) + +func migrateRunTasks(tx *sql.Tx, _ string) error { + rows, err := tx.Query(`SELECT workflow_run FROM tasks WHERE workflow_run != '' GROUP BY workflow_run ORDER BY min(rowid)`) + if err != nil { + return err + } + runs := []string{} + for rows.Next() { + run := "" + err = rows.Scan(&run) + if err != nil { + rows.Close() + return err + } + runs = append(runs, run) + } + rows.Close() + err = rows.Err() + if err != nil { + return err + } + + for _, run := range runs { + id := ids.Derive(ids.Task, run) + _, err = tx.Exec(` + INSERT INTO tasks (id, project_id, title, branch, worktree_path, base_ref, status, created_by, created_at, closed_at, todo_item, color, + workflow_name, workflow_definition_id, workflow_brief, workflow_title, audit) + SELECT ?1, f.project_id, f.workflow_title, '', '', '', + CASE + WHEN EXISTS (SELECT 1 FROM tasks s WHERE s.workflow_run = ?2 AND s.status = 'active') THEN 'active' + WHEN EXISTS (SELECT 1 FROM tasks s WHERE s.workflow_run = ?2 AND s.status = 'merged') THEN 'merged' + ELSE 'discarded' + END, + f.created_by, f.created_at, + CASE WHEN EXISTS (SELECT 1 FROM tasks s WHERE s.workflow_run = ?2 AND s.status = 'active') THEN NULL + ELSE (SELECT max(s.closed_at) FROM tasks s WHERE s.workflow_run = ?2) END, + f.todo_item, f.color, f.workflow_name, f.workflow_definition_id, f.workflow_brief, f.workflow_title, f.audit + FROM tasks f WHERE f.workflow_run = ?2 ORDER BY f.created_at LIMIT 1`, id, run) + if err != nil { + return fmt.Errorf("run %s: %w", run, err) + } + _, err = tx.Exec(` + UPDATE events SET task_id = ?1, payload = json_set(payload, '$.id', ?1) + WHERE kind = 'task.workflow_finished' AND task_id IN (SELECT id FROM tasks WHERE workflow_run = ?2)`, id, run) + if err != nil { + return fmt.Errorf("run %s: %w", run, err) + } + _, err = tx.Exec(` + UPDATE tasks SET parent_task_id = ?1, workflow_name = '', workflow_definition_id = NULL, workflow_brief = '', workflow_title = '', audit = '', todo_item = '' + WHERE workflow_run = ?2`, id, run) + if err != nil { + return fmt.Errorf("run %s: %w", run, err) + } + _, err = tx.Exec(`UPDATE tasks SET state = `+taskStateExpr+` WHERE id = ?`, id) + if err != nil { + return fmt.Errorf("run %s: %w", run, err) + } + } + + _, err = tx.Exec(`ALTER TABLE tasks DROP COLUMN workflow_run`) + return err +} diff --git a/internal/store/store.go b/internal/store/store.go index da80e71..c33c245 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -35,6 +35,7 @@ var goMigrations = map[string]func(tx *sql.Tx, dataDir string) error{ "071-workflow-steps": migrateWorkflowSteps, "078-normalize-workflow-definitions": migrateWorkflowDefinitions, "085-prefix-ids": migratePrefixIDs, + "088-workflow-run-tasks": migrateRunTasks, } func loadMigrations() ([]migration, error) { @@ -331,16 +332,15 @@ func (s *Store) CreateTask(ctx context.Context, t api.Task, frozen *FrozenWorkfl var workflowDefinitionID *string workflowBrief := "" workflowTitle := "" - workflowRun := "" - workflowStage := "" if frozen != nil { workflowName = frozen.Name workflowDefinitionID = &frozen.DefinitionID workflowBrief = frozen.Brief workflowTitle = frozen.Title - workflowRun = *t.WorkflowRun + } + workflowStage := "" + if t.WorkflowStage != nil { workflowStage = *t.WorkflowStage - t.Workflow = &workflowName } todoItem := "" if t.TodoItem != nil { @@ -365,7 +365,6 @@ func (s *Store) CreateTask(ctx context.Context, t api.Task, frozen *FrozenWorkfl audit := "" if t.Audit != nil { audit = *t.Audit - t.Background = true } return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { if parentTaskID != "" { @@ -379,16 +378,18 @@ func (s *Store) CreateTask(ctx context.Context, t api.Task, frozen *FrozenWorkfl return ErrParentNotOpen } } - err := tx.QueryRowContext(ctx, ` - SELECT min(candidate) FROM ( - SELECT ? AS candidate - UNION ALL SELECT port_base + ? FROM tasks WHERE status = ? - ) WHERE candidate NOT IN (SELECT port_base FROM tasks WHERE status = ?) - `, s.portFirst, PortBlock, string(api.TaskStatusActive), string(api.TaskStatusActive)).Scan(&t.PortBase) - if err != nil { - return err + if t.WorktreePath != "" { + err := tx.QueryRowContext(ctx, ` + SELECT min(candidate) FROM ( + SELECT ? AS candidate + UNION ALL SELECT port_base + ? FROM tasks WHERE status = ? AND port_base != 0 + ) WHERE candidate NOT IN (SELECT port_base FROM tasks WHERE status = ?) + `, s.portFirst, PortBlock, string(api.TaskStatusActive), string(api.TaskStatusActive)).Scan(&t.PortBase) + if err != nil { + return err + } } - err = tx.QueryRowContext(ctx, ` + err := tx.QueryRowContext(ctx, ` WITH RECURSIVE palette(c) AS (SELECT 0 UNION ALL SELECT c + 1 FROM palette WHERE c + 1 < ?) SELECT c FROM palette ORDER BY c IN (SELECT color FROM tasks WHERE status = ?), coalesce((SELECT max(rowid) FROM tasks WHERE color = c), -1), c @@ -398,9 +399,13 @@ func (s *Store) CreateTask(ctx context.Context, t api.Task, frozen *FrozenWorkfl return err } _, err = tx.ExecContext(ctx, ` - INSERT INTO tasks (id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, todo_item, forked_from, forked_at, workflow_name, workflow_definition_id, workflow_brief, workflow_title, workflow_run, workflow_stage, parent_task_id, audit) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, t.Id, t.ProjectId, t.Title, mergeTitle, t.Branch, t.WorktreePath, t.BaseRef, t.PortBase, t.Color, string(t.Status), t.CreatedBy, formatTime(t.CreatedAt), todoItem, forkedFrom, forkedAt, workflowName, workflowDefinitionID, workflowBrief, workflowTitle, workflowRun, workflowStage, parentTaskID, audit) + INSERT INTO tasks (id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, todo_item, forked_from, forked_at, workflow_name, workflow_definition_id, workflow_brief, workflow_title, workflow_stage, parent_task_id, audit) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, t.Id, t.ProjectId, t.Title, mergeTitle, t.Branch, t.WorktreePath, t.BaseRef, t.PortBase, t.Color, string(t.Status), t.CreatedBy, formatTime(t.CreatedAt), todoItem, forkedFrom, forkedAt, workflowName, workflowDefinitionID, workflowBrief, workflowTitle, workflowStage, parentTaskID, audit) + if err != nil { + return err + } + t, err = scanTask(tx.QueryRowContext(ctx, `SELECT `+taskColumns+` FROM tasks WHERE id = ?`, t.Id)) if err != nil { return err } @@ -954,7 +959,11 @@ func (s *Store) closeTask(ctx context.Context, id string, status api.TaskStatus, if err != nil { return err } - return dismissTaskInbox(ctx, tx, emit, id, actor) + err = dismissTaskInbox(ctx, tx, emit, id, actor) + if err != nil { + return err + } + return closeRun(ctx, tx, emit, id, actor) }) if err != nil { return api.Task{}, err @@ -962,6 +971,24 @@ func (s *Store) closeTask(ctx context.Context, id string, status api.TaskStatus, return s.GetTask(ctx, id) } +func closeRun(ctx context.Context, tx *sql.Tx, emit emitFunc, stageID, actor string) error { + runID := "" + err := tx.QueryRowContext(ctx, ` + UPDATE tasks SET closed_at = ?, + status = CASE WHEN EXISTS (SELECT 1 FROM tasks c WHERE c.parent_task_id = tasks.id AND c.status = ?) THEN ? ELSE ? END + WHERE id = (SELECT parent_task_id FROM tasks WHERE id = ? AND workflow_stage != '') AND status = ? + AND NOT EXISTS (SELECT 1 FROM tasks c WHERE c.parent_task_id = tasks.id AND c.status = ?) + RETURNING id`, + now(), string(api.TaskStatusMerged), string(api.TaskStatusMerged), string(api.TaskStatusDiscarded), stageID, string(api.TaskStatusActive), string(api.TaskStatusActive)).Scan(&runID) + if errors.Is(err, sql.ErrNoRows) { + return nil + } + if err != nil { + return err + } + return settleTaskState(ctx, tx, emit, runID, actor) +} + func (s *Store) Participants(ctx context.Context, taskID string) ([]api.Participant, error) { rows, err := s.db.QueryContext(ctx, ` SELECT u.login, u.display_name, u.git_name, u.git_email @@ -1583,7 +1610,8 @@ func settleTaskState(ctx context.Context, tx *sql.Tx, emit emitFunc, taskID, act projectID := "" before := "" now := "" - err := tx.QueryRowContext(ctx, `SELECT project_id, state, `+taskStateExpr+` FROM tasks WHERE id = ?`, taskID).Scan(&projectID, &before, &now) + runID := "" + err := tx.QueryRowContext(ctx, `SELECT project_id, state, `+taskStateExpr+`, CASE WHEN workflow_stage != '' THEN parent_task_id ELSE '' END FROM tasks WHERE id = ?`, taskID).Scan(&projectID, &before, &now, &runID) if errors.Is(err, sql.ErrNoRows) { return nil } @@ -1597,17 +1625,26 @@ func settleTaskState(ctx context.Context, tx *sql.Tx, emit emitFunc, taskID, act if err != nil { return err } - return emit(api.Event{ + err = emit(api.Event{ Kind: "task.state", ProjectId: &projectID, TaskId: &taskID, Actor: actor, Payload: map[string]any{"from": before, "to": now}, }) + if err != nil || runID == "" { + return err + } + return settleTaskState(ctx, tx, emit, runID, actor) } const taskStateExpr = `CASE WHEN tasks.status != 'active' THEN tasks.status + WHEN tasks.workflow_name != '' THEN CASE + WHEN EXISTS (SELECT 1 FROM events e WHERE e.task_id = tasks.id AND e.kind = 'task.workflow_finished') THEN 'finished' + WHEN EXISTS (SELECT 1 FROM tasks c WHERE c.parent_task_id = tasks.id AND c.status = 'active' AND c.state = 'running') THEN 'running' + ELSE 'idle' + END WHEN tasks.discard_state != '' THEN 'discarding' WHEN EXISTS (SELECT 1 FROM inbox_items i WHERE i.task_id = tasks.id AND i.state = 'open') THEN 'asking' WHEN tasks.merging_by != '' OR tasks.finalizing != '' OR tasks.parent_merge_by != '' THEN 'merging' @@ -1683,9 +1720,11 @@ func (s *Store) ClearReviewRequests(ctx context.Context) error { const projectColumns = `id, name, repo_path, default_branch, remote, created_by, created_at, review_workflow, audit_workflow` -const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_run, workflow_stage, parent_task_id, parent_merge_by, merge_after_review_by != '', state, merging_by != '' OR finalizing != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1), audit, ` + backgroundExpr +const taskColumns = `id, project_id, title, merge_title, branch, worktree_path, base_ref, port_base, color, status, created_by, created_at, closed_at, merge_commit, todo_item, forked_from, forked_at, merge_queued_at, workflow_name, workflow_stage, parent_task_id, parent_merge_by, merge_after_review_by != '', state, merging_by != '' OR finalizing != '', discard_state != '' AND status = 'active', (SELECT coalesce(max(seq), 0) FROM events WHERE events.task_id = tasks.id AND kind = 'turn.ended' AND json_extract(payload, '$.summary') = 1), audit, ` + backgroundExpr -const backgroundExpr = `audit != '' AND NOT EXISTS (SELECT 1 FROM events WHERE events.task_id = tasks.id AND kind IN ('inbox.created', 'task.ready', 'session.error', 'session.rate_limited'))` +const backgroundExpr = `(audit != '' OR EXISTS (SELECT 1 FROM tasks r WHERE r.id = tasks.parent_task_id AND tasks.workflow_stage != '' AND r.audit != '')) + AND NOT EXISTS (SELECT 1 FROM events WHERE events.task_id IN (SELECT c.id FROM tasks c WHERE c.id = tasks.id OR (c.parent_task_id = tasks.id AND tasks.workflow_name != '')) + AND kind IN ('inbox.created', 'task.ready', 'session.error', 'session.rate_limited'))` const sessionColumns = `id, coalesce(task_id, ''), owner, vendor, model, vendor_session_id, state, role, member, created_by, created_at, forked_from, forked_at, subagents` @@ -1715,13 +1754,12 @@ func scanTask(row scanner) (api.Task, error) { forkedAt := 0 mergeQueuedAt := "" workflowName := "" - workflowRun := "" workflowStage := "" parentTaskID := "" parentMergeBy := "" taskState := "" audit := "" - err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt, &workflowName, &workflowRun, &workflowStage, &parentTaskID, &parentMergeBy, &t.MergeAfterReview, &taskState, &t.Merging, &t.Discarding, &t.SummarySeq, &audit, &t.Background) + err := row.Scan(&t.Id, &t.ProjectId, &t.Title, &t.MergeTitle, &t.Branch, &t.WorktreePath, &t.BaseRef, &t.PortBase, &t.Color, &status, &t.CreatedBy, &createdAt, &closedAt, &mergeCommit, &todoItem, &forkedFrom, &forkedAt, &mergeQueuedAt, &workflowName, &workflowStage, &parentTaskID, &parentMergeBy, &t.MergeAfterReview, &taskState, &t.Merging, &t.Discarding, &t.SummarySeq, &audit, &t.Background) if err != nil { return t, err } @@ -1735,7 +1773,8 @@ func scanTask(row scanner) (api.Task, error) { t.MergingIntoParent = parentMergeBy != "" if workflowName != "" { t.Workflow = &workflowName - t.WorkflowRun = &workflowRun + } + if workflowStage != "" { t.WorkflowStage = &workflowStage } if t.MergeTitle == "" { diff --git a/internal/store/workflow.go b/internal/store/workflow.go index 6a71701..d236605 100644 --- a/internal/store/workflow.go +++ b/internal/store/workflow.go @@ -160,7 +160,10 @@ func (s *Store) FreezeWorkflow(ctx context.Context, name string) (FrozenWorkflow func (s *Store) TaskWorkflow(ctx context.Context, taskID string) (FrozenWorkflow, error) { f := FrozenWorkflow{} defID := sql.NullString{} - err := s.db.QueryRowContext(ctx, `SELECT workflow_name, workflow_definition_id, workflow_brief, workflow_title, audit FROM tasks WHERE id = ?`, taskID).Scan(&f.Name, &defID, &f.Brief, &f.Title, &f.Audit) + err := s.db.QueryRowContext(ctx, ` + SELECT r.workflow_name, r.workflow_definition_id, r.workflow_brief, r.workflow_title, r.audit + FROM tasks t JOIN tasks r ON r.id = CASE WHEN t.workflow_stage != '' THEN t.parent_task_id ELSE t.id END + WHERE t.id = ?`, taskID).Scan(&f.Name, &defID, &f.Brief, &f.Title, &f.Audit) if errors.Is(err, sql.ErrNoRows) { return FrozenWorkflow{}, fmt.Errorf("%w: no task %s", ErrNotFound, taskID) } @@ -187,13 +190,13 @@ func (s *Store) LastAudit(ctx context.Context, projectID string) (string, bool, } func (s *Store) RunTasks(ctx context.Context, run string) ([]api.Task, error) { - return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE workflow_run = ? ORDER BY created_at`, run) + return s.queryTasks(ctx, `SELECT `+taskColumns+` FROM tasks WHERE parent_task_id = ? AND workflow_stage != '' ORDER BY created_at`, run) } func (s *Store) RunEvents(ctx context.Context, run string) ([]api.Event, error) { rows, err := s.db.QueryContext(ctx, ` SELECT `+eventColumns+` FROM events - WHERE task_id IN (SELECT id FROM tasks WHERE workflow_run = ?) AND kind IN ('task.workflow_stage', 'task.discarded', 'task.workflow_finished') + WHERE task_id IN (SELECT id FROM tasks WHERE parent_task_id = ?1 AND workflow_stage != '') AND kind IN ('task.workflow_stage', 'task.discarded') ORDER BY seq `, run) if err != nil { @@ -283,7 +286,7 @@ func clearStage(ctx context.Context, tx *sql.Tx, sessionID string) error { return err } -func (s *Store) FinishWorkflow(ctx context.Context, task api.Task, sessionID, workflowName, output string, kept []string) error { +func (s *Store) FinishWorkflow(ctx context.Context, run api.Task, sessionID, workflowName, output string, kept []string) error { return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { err := clearStage(ctx, tx, sessionID) if err != nil { @@ -291,16 +294,16 @@ func (s *Store) FinishWorkflow(ctx context.Context, task api.Task, sessionID, wo } return emit(api.Event{ Kind: "task.workflow_finished", - ProjectId: &task.ProjectId, - TaskId: &task.Id, + ProjectId: &run.ProjectId, + TaskId: &run.Id, Actor: "byre", - Payload: map[string]any{"id": task.Id, "workflow": workflowName, "output": output, "kept": kept}, + Payload: map[string]any{"id": run.Id, "workflow": workflowName, "output": output, "kept": kept}, }) }) } func (s *Store) WorkflowFinished(ctx context.Context, run string) (bool, error) { finished := false - err := s.db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM events e JOIN tasks t ON t.id = e.task_id WHERE t.workflow_run = ? AND e.kind = 'task.workflow_finished')`, run).Scan(&finished) + err := s.db.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM events WHERE task_id = ? AND kind = 'task.workflow_finished')`, run).Scan(&finished) return finished, err } diff --git a/mac/Sources/Byre/Chat.swift b/mac/Sources/Byre/Chat.swift index 2dcc76d..9044c3a 100644 --- a/mac/Sources/Byre/Chat.swift +++ b/mac/Sources/Byre/Chat.swift @@ -661,7 +661,7 @@ private struct MergeButton: View { ? "Waiting for the merge in progress in this project to finish" : task.mergeAfterReview ? "Merges when the running review finishes with no findings; open to merge now instead (⌘S)" - : task.parentTaskId != nil + : task.parentTaskId != nil && task.workflowStage == nil ? "Merge this subtask into its parent task's branch, or into \(target) (⌘S)" : "The agent merges \(target) in, then byre squashes into one commit on \(target) and pushes it to origin; held while another task in the project is merging (⌘S)" Button(intoParent ? "Merging into parent" : held ? "Merge held" : task.mergeAfterReview ? "Merge after review" : keepOpen == false ? "Merge & complete" : "Merge") { diff --git a/mac/Sources/Byre/MergeDialog.swift b/mac/Sources/Byre/MergeDialog.swift index 6285a93..2595aa1 100644 --- a/mac/Sources/Byre/MergeDialog.swift +++ b/mac/Sources/Byre/MergeDialog.swift @@ -91,7 +91,7 @@ func confirmMerge(_ taskId: String, _ store: Store, _ overlays: Overlays, keepOp return } let target = store.projects.first { $0.id == task.projectId }?.defaultBranch ?? "the default branch" - let parent = task.parentTaskId.flatMap { store.findTask($0) } + let parent = task.workflowStage == nil ? task.parentTaskId.flatMap { store.findTask($0) } : nil let coAuthors: [Participant] do { let client = store.api.client diff --git a/mac/Sources/Byre/Panes.swift b/mac/Sources/Byre/Panes.swift index 88622c6..27458bf 100644 --- a/mac/Sources/Byre/Panes.swift +++ b/mac/Sources/Byre/Panes.swift @@ -195,7 +195,7 @@ private struct TaskPane: View { @ViewBuilder private var tabBody: some View { - switch store.focus.tab { + switch store.shownTab { case .transcript: ChatView(kind: .task(store.selectedTaskId)) .id("\(store.selectedTaskId ?? "")/\(store.selectedTaskId.flatMap { store.pickedMember($0) } ?? "")") @@ -342,16 +342,19 @@ private struct TabStrip: View { } private func info() -> Info { - let tab = store.focus.tab - let workflow = store.selectedTaskId.flatMap { store.findTask($0) }?.workflowRun != nil + let tab = store.shownTab + let run = store.selectedTaskId.flatMap { store.findTask($0) }?.workflow != nil let team = store.selectedTaskId.map { !store.team($0).isEmpty } ?? false let order = taskTabs.filter { t in + if run { + return t == .workflow || t == .project + } if t == tab { return true } switch t { case .board: return team - case .workflow: return workflow + case .workflow: return false default: return true } } diff --git a/mac/Sources/Byre/Project.swift b/mac/Sources/Byre/Project.swift index 2aa908e..acfa441 100644 --- a/mac/Sources/Byre/Project.swift +++ b/mac/Sources/Byre/Project.swift @@ -84,7 +84,7 @@ struct ProjectView: View { return } let active = (store.tasks[p.id] ?? []).filter { $0.status == .active } - let rows = nested(store, active.filter { !$0.background }).map(\.task.id) + active.filter(\.background).map(\.id) + let rows = nested(store, active).map(\.task.id) if rows.isEmpty { return } @@ -200,11 +200,7 @@ private struct ProjectBlock: View { .padding(.horizontal, 10) .padding(.vertical, 6) } - TaskRows(active: active.filter { !$0.background }) - ForEach(active.filter(\.background), id: \.id) { task in - BackgroundRow(task: task) - .id(task.id) - } + TaskRows(active: active) heading("TODO") TodoBlock(project: project, todo: todo, active: active) } @@ -229,12 +225,17 @@ private func nested(_ store: Store, _ active: [ByreTask]) -> [(task: ByreTask, d add(child, depth + 1) } } - for task in store.byRank(active.filter { $0.parentTaskId == nil || !ids.contains($0.parentTaskId!) }) { + let roots = active.filter { $0.parentTaskId == nil || !ids.contains($0.parentTaskId!) } + for task in store.byRank(roots.filter { !$0.background }) + roots.filter(\.background) { add(task, 0) } return out } +private func rowIndent(_ task: ByreTask, _ depth: Int) -> CGFloat { + task.parentTaskId != nil ? 20 * CGFloat(depth - 1) : 0 +} + private struct TaskRows: View { let active: [ByreTask] @Environment(Store.self) private var store @@ -243,8 +244,13 @@ private struct TaskRows: View { if !active.isEmpty { VStack(alignment: .leading, spacing: 0) { ForEach(nested(store, active), id: \.task.id) { row in - TaskRow(task: row.task, depth: row.depth) - .id(row.task.id) + if row.task.background { + BackgroundRow(task: row.task, depth: row.depth) + .id(row.task.id) + } else { + TaskRow(task: row.task, depth: row.depth) + .id(row.task.id) + } } } .padding(.bottom, 8) @@ -285,14 +291,10 @@ private struct TaskRow: View { let ts = store.taskState(task) let color = taskPalette[task.color] let count = store.inbox.filter { $0.taskId == task.id }.count - HoverRow(selected: task.id == store.selectedTaskId, indent: task.parentTaskId != nil ? 20 * CGFloat(depth - 1) : 0) { + HoverRow(selected: task.id == store.selectedTaskId, indent: rowIndent(task, depth)) { store.focusTask(task.id) } content: { - if let parent = task.parentTaskId { - Text("↳") - .foregroundStyle(Palette.muted) - .help("Subtask of \(store.findTask(parent)?.name ?? parent)") - } + SubtaskMark(task: task) StateMark(state: ts, color: color) Text(task.name) .foregroundStyle(color) @@ -318,15 +320,30 @@ private struct TaskRow: View { } } -private struct BackgroundRow: View { +private struct SubtaskMark: View { let task: ByreTask @Environment(Store.self) private var store + var body: some View { + if let parent = task.parentTaskId { + Text("↳") + .foregroundStyle(Palette.muted) + .help("Subtask of \(store.findTask(parent)?.name ?? parent)") + } + } +} + +private struct BackgroundRow: View { + let task: ByreTask + let depth: Int + @Environment(Store.self) private var store + var body: some View { let ts = store.taskState(task) - HoverRow(selected: task.id == store.selectedTaskId) { + HoverRow(selected: task.id == store.selectedTaskId, indent: rowIndent(task, depth)) { store.focusTask(task.id) } content: { + SubtaskMark(task: task) Text(task.name) .lineLimit(1) .truncationMode(.tail) diff --git a/mac/Sources/Byre/Store.swift b/mac/Sources/Byre/Store.swift index a2452ac..a42a032 100644 --- a/mac/Sources/Byre/Store.swift +++ b/mac/Sources/Byre/Store.swift @@ -391,6 +391,14 @@ final class Store { projects.first { $0.id == focus.projectId } } + var shownTab: TaskTab { + let run = selectedTaskId.flatMap { findTask($0) }?.workflow != nil + if run { + return focus.tab == .project ? .project : .workflow + } + return focus.tab == .workflow ? .transcript : focus.tab + } + func setFocus(taskId: String?? = .none, projectId: String?? = .none, tab: TaskTab? = nil) async throws { let task = taskId ?? focus.taskId let project = task != nil ? findTask(task!)?.projectId : (projectId ?? focus.projectId) @@ -527,7 +535,7 @@ final class Store { } func unreadSummary(_ task: ByreTask) -> Bool { - task.workflow == nil && task.summarySeq > (summaryReads[task.id] ?? 0) + task.workflowStage == nil && task.summarySeq > (summaryReads[task.id] ?? 0) } var unreadSummaries: Int { diff --git a/mac/Sources/Byre/Workflow.swift b/mac/Sources/Byre/Workflow.swift index abd5a96..2886b94 100644 --- a/mac/Sources/Byre/Workflow.swift +++ b/mac/Sources/Byre/Workflow.swift @@ -66,6 +66,7 @@ private func stateColor(_ state: String) -> Color { struct WorkflowView: View { @Environment(Store.self) private var store @ViewState private var run: WorkflowRun? + @ViewState private var runId: String? @ViewState private var error = "" @ViewState private var target = 0 @ViewState private var loading = false @@ -74,8 +75,8 @@ struct WorkflowView: View { var body: some View { let task = store.selectedTaskId.flatMap { store.findTask($0) } VStack(spacing: 0) { - if let task, let runId = task.workflowRun { - if let run, run.run == runId { + if let task, task.workflow != nil { + if let run, runId == task.id { HStack(alignment: .firstTextBaseline, spacing: 10) { Text(run.name).font(Fonts.font(13, weight: 700)) Text(run.title).foregroundStyle(Palette.muted) @@ -114,13 +115,14 @@ struct WorkflowView: View { EmptyText(text: "Loading…") } } else { - EmptyText(text: task != nil ? "This task isn't part of a workflow run." : "No task selected.") + EmptyText(text: task != nil ? "This task isn't a workflow run." : "No task selected.") } } .frame(maxHeight: .infinity, alignment: .top) .task(id: store.selectedTaskId) { target += 1 run = nil + runId = nil error = "" await load() let off = store.events.add { ev in @@ -134,7 +136,7 @@ struct WorkflowView: View { off() target += 1 } - .onChange(of: task?.workflowRun) { + .onChange(of: task?.workflow) { if run == nil && !loading && error.isEmpty { Task { await load() @@ -144,13 +146,13 @@ struct WorkflowView: View { } private func relevant(_ ev: ByreEvent) -> Bool { - guard ev.seq != 0, let run else { + guard ev.seq != 0, let run, let runId else { return false } if ev.kind == "task.created" { - return ev.payload.value["workflow_run"] as? String == run.run + return ev.payload.value["parent_task_id"] as? String == runId } - let ids = Set(run.stages.flatMap { $0.attempts.map(\.taskId) }) + let ids = Set(run.stages.flatMap { $0.attempts.map(\.taskId) } + [runId]) guard let taskId = ev.taskId else { return false } @@ -158,7 +160,7 @@ struct WorkflowView: View { } private func load() async { - guard let taskId = store.selectedTaskId, store.findTask(taskId)?.workflowRun != nil else { + guard let taskId = store.selectedTaskId, store.findTask(taskId)?.workflow != nil else { return } if loading { @@ -172,6 +174,7 @@ struct WorkflowView: View { let fresh = try await store.api.client.getWorkflowRun(path: .init(taskId: taskId)).ok.body.json if started == target { run = fresh + runId = taskId error = "" } } catch {