2026-10-14 19:35:34 -07:00
package agents
import (
"context"
"errors"
"fmt"
2026-10-16 10:36:38 -07:00
"log/slog"
2026-10-14 19:35:34 -07:00
"path/filepath"
"time"
"g.fc.run/misc/byre/internal/api"
"g.fc.run/misc/byre/internal/git"
"g.fc.run/misc/byre/internal/store"
2026-10-15 07:23:17 -07:00
"g.fc.run/misc/byre/internal/todo"
2026-10-14 19:35:34 -07:00
)
2026-10-15 22:53:54 -07:00
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 )
2026-10-15 07:23:17 -07:00
}
2026-10-17 08:10:55 -07:00
func ( m * Manager ) StartTask ( ctx context . Context , project api . Project , creator api . User , title , prompt , model string ) ( api . Task , error ) {
if creator . GitName == "" || creator . GitEmail == "" {
return api . Task {}, ErrNoGitIdentity
}
task , err := m . CreateTask ( ctx , project , title , creator . Login , project . DefaultBranch , creator . Login )
if err != nil {
return api . Task {}, err
}
2026-10-18 08:48:57 -07:00
_ , err = m . create ( ctx , task , creator , "" , prompt , model , creator . Login , nil )
2026-10-17 08:10:55 -07:00
if err != nil {
return api . Task {}, fmt . Errorf ( "task %s created, but its agent did not start: %w" , task . Id , err )
}
return task , nil
}
2026-10-16 08:53:02 -07:00
func ( m * Manager ) StartTodo ( ctx context . Context , project api . Project , creator api . User , title , model , actor , workflowName string , discuss bool ) ([] api . Task , error ) {
2026-10-15 07:23:17 -07:00
if creator . GitName == "" || creator . GitEmail == "" {
2026-10-15 22:53:54 -07:00
return nil , ErrNoGitIdentity
2026-10-15 07:23:17 -07:00
}
src , found , err := git . ReadFile ( project . RepoPath , project . DefaultBranch , todo . File )
if err != nil {
2026-10-15 22:53:54 -07:00
return nil , err
2026-10-15 07:23:17 -07:00
}
if ! found {
2026-10-15 22:53:54 -07:00
return nil , fmt . Errorf ( "%w: %s has no %s" , store . ErrNotFound , project . DefaultBranch , todo . File )
2026-10-15 07:23:17 -07:00
}
item , found := todo . Find ( todo . Parse ( src ), title )
if ! found {
2026-10-15 22:53:54 -07:00
return nil , fmt . Errorf ( "%w: %s on %s has no item %q" , store . ErrNotFound , todo . File , project . DefaultBranch , title )
2026-10-15 07:23:17 -07:00
}
tasks , err := m . store . ListTasks ( ctx , project . Id )
if err != nil {
2026-10-15 22:53:54 -07:00
return nil , err
2026-10-15 07:23:17 -07:00
}
for _ , t := range tasks {
if t . Status == api . TaskStatusActive && t . TodoItem != nil && * t . TodoItem == title {
2026-10-15 22:53:54 -07:00
return nil , fmt . Errorf ( "%w: task %s is already working on %q" , ErrConflict , t . Id , title )
2026-10-15 07:23:17 -07:00
}
}
2026-10-15 22:53:54 -07:00
started := [] api . Task {}
if workflowName != "" {
2026-10-16 08:53:02 -07:00
started , err = m . StartWorkflow ( ctx , project , creator , item . Title , project . DefaultBranch , actor , workflowName , todoPrompt ( item , discuss ), & item . Title )
2026-10-15 22:53:54 -07:00
if err != nil {
return nil , err
}
2026-10-15 22:11:25 -07:00
} else {
2026-10-15 22:53:54 -07:00
task , err := m . createTask ( ctx , project , api . Task { Title : item . Title , CreatedBy : creator . Login , TodoItem : & item . Title }, nil , project . DefaultBranch , actor )
if err != nil {
return nil , err
}
2026-10-18 08:48:57 -07:00
_ , err = m . create ( ctx , task , creator , "" , todoPrompt ( item , discuss ), model , actor , nil )
2026-10-15 22:11:25 -07:00
if err != nil {
2026-10-15 22:53:54 -07:00
return nil , fmt . Errorf ( "task %s created, but its agent did not start: %w" , task . Id , err )
2026-10-15 22:11:25 -07:00
}
2026-10-15 22:53:54 -07:00
started = append ( started , task )
2026-10-15 07:23:17 -07:00
}
2026-10-15 19:40:44 -07:00
if actor == creator . Login {
m . setStartedTodo ( creator . Login , true )
}
2026-10-15 22:53:54 -07:00
return started , nil
2026-10-15 07:23:17 -07:00
}
2026-10-16 08:53:02 -07:00
func todoPrompt ( item api . TodoItem , discuss bool ) string {
p := fmt . Sprintf ( "Work on this item from %[1]s at the repo root:\n\n%[2]s\n\nWhen the work is done, remove this item from %[1]s in this worktree, so the removal lands in the same merge as the work." ,
2026-10-15 07:23:17 -07:00
todo . File , todo . Markdown ( item ))
2026-10-16 08:53:02 -07:00
if ! discuss {
return p
}
2026-10-16 09:32:07 -07:00
return p + "\n\nThe human wants to discuss this item with you before any of it is built: a conversation, not a questionnaire. Read what you need to understand it, then end your turn with your thoughts on it in your final message: how you read it, how you would approach it, and what trade-offs or doubts you see. Don't use ask_human for this; the human replies in the chat. Keep the conversation going turn by turn, and change nothing until they tell you to build it."
2026-10-15 07:23:17 -07:00
}
2026-10-15 21:17:58 -07:00
func ( m * Manager ) createTask ( ctx context . Context , project api . Project , t api . Task , frozen * store . FrozenWorkflow , base , actor string ) ( api . Task , error ) {
2026-10-15 11:36:51 -07:00
t . Id = store . NewID ()
t . ProjectId = project . Id
if t . MergeTitle == "" {
t . MergeTitle = t . Title
2026-10-14 19:35:34 -07:00
}
2026-10-15 11:36:51 -07:00
t . Branch = "byre/" + t . Id
t . WorktreePath = filepath . Join ( m . worktreeRoot , project . Id , t . Id )
t . Status = api . TaskStatusActive
t . CreatedAt = time . Now (). UTC ()
2026-10-14 19:35:34 -07:00
var err error
t . BaseRef , err = git . AddWorktree ( project . RepoPath , t . WorktreePath , t . Branch , base )
if err != nil {
return api . Task {}, err
}
2026-10-15 21:17:58 -07:00
err = m . store . CreateTask ( ctx , t , frozen , actor )
2026-10-14 19:35:34 -07:00
if err != nil {
2026-10-16 10:36:38 -07:00
rerr := git . RemoveWorktree ( project . RepoPath , t . WorktreePath , t . Branch )
if rerr != nil {
slog . Error ( "remove worktree of task not created" , "task" , t . Id , "err" , rerr )
}
if errors . Is ( err , store . ErrParentNotOpen ) {
return api . Task {}, fmt . Errorf ( "%w: %w" , ErrConflict , err )
}
2026-10-14 19:35:34 -07:00
return api . Task {}, err
}
2026-10-15 13:15:49 -07:00
return m . store . GetTask ( ctx , t . Id )
2026-10-14 19:35:34 -07:00
}