2026-10-14 19:35:34 -07:00
package agents
import (
"context"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"strings"
"time"
"g.fc.run/misc/byre/internal/api"
"g.fc.run/misc/byre/internal/git"
"g.fc.run/misc/byre/internal/mcp"
"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
)
func ( m * Manager ) CreateTask ( ctx context . Context , project api . Project , title , createdBy , base string ) ( api . Task , error ) {
2026-10-15 07:23:17 -07:00
return m . createTask ( ctx , project , title , createdBy , base , nil )
}
func ( m * Manager ) StartTodo ( ctx context . Context , project api . Project , creator api . User , title , model string ) ( api . Task , error ) {
if creator . GitName == "" || creator . GitEmail == "" {
return api . Task {}, ErrNoGitIdentity
}
src , found , err := git . ReadFile ( project . RepoPath , project . DefaultBranch , todo . File )
if err != nil {
return api . Task {}, err
}
if ! found {
return api . Task {}, fmt . Errorf ( "%w: %s has no %s" , store . ErrNotFound , project . DefaultBranch , todo . File )
}
item , found := todo . Find ( todo . Parse ( src ), title )
if ! found {
return api . Task {}, fmt . Errorf ( "%w: %s on %s has no item %q" , store . ErrNotFound , todo . File , project . DefaultBranch , title )
}
tasks , err := m . store . ListTasks ( ctx , project . Id )
if err != nil {
return api . Task {}, err
}
for _ , t := range tasks {
if t . Status == api . TaskStatusActive && t . TodoItem != nil && * t . TodoItem == title {
return api . Task {}, fmt . Errorf ( "%w: task %s is already working on %q" , ErrConflict , t . Id , title )
}
}
task , err := m . createTask ( ctx , project , item . Title , creator . Login , project . DefaultBranch , & item . Title )
if err != nil {
return api . Task {}, err
}
_ , err = m . create ( ctx , task , creator , todoPrompt ( item ), model , creator . Login , nil )
if err != nil {
return api . Task {}, fmt . Errorf ( "task %s created, but its agent did not start: %w" , task . Id , err )
}
return task , nil
}
func todoPrompt ( item api . TodoItem ) string {
return 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." ,
todo . File , todo . Markdown ( item ))
}
func ( m * Manager ) createTask ( ctx context . Context , project api . Project , title , createdBy , base string , todoItem * string ) ( api . Task , error ) {
2026-10-14 19:35:34 -07:00
id := store . NewID ()
t := api . Task {
Id : id ,
ProjectId : project . Id ,
Title : title ,
2026-10-14 21:21:54 -07:00
MergeTitle : title ,
2026-10-14 19:35:34 -07:00
Branch : "byre/" + id ,
WorktreePath : filepath . Join ( m . worktreeRoot , project . Id , id ),
Status : api . TaskStatusActive ,
CreatedBy : createdBy ,
CreatedAt : time . Now (). UTC (),
2026-10-15 07:23:17 -07:00
TodoItem : todoItem ,
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
}
err = m . store . CreateTask ( ctx , t )
if err != nil {
return api . Task {}, err
}
return t , nil
}
type createTaskArgs struct {
Title string `json:"title"`
Prompt string `json:"prompt"`
Base string `json:"base"`
Model string `json:"model"`
}
func ( m * Manager ) createTaskTool ( l * live ) mcp . Tool {
return mcp . Tool {
Name : "create_task" ,
Description : "Create a new byre task in this project, with its own branch and worktree, and start an agent on it with prompt as its first message. " +
"Use it to run independent pieces of work in parallel. The new agent sees nothing of your context, so the prompt must be a complete brief. " +
"base picks the starting commit: default_branch starts from the project's default branch; this_task starts from this task's HEAD, " +
"which includes your committed work but not uncommitted edits (byre commits at the end of each turn). " +
"Returns at once with the new task's id; the new agent runs on its own." ,
InputSchema : json . RawMessage ( `{
"type": "object",
"properties": {
"title": {"type": "string", "description": "Short task title, shown in the task list."},
"prompt": {"type": "string", "description": "The new agent's first message: a complete, self-contained brief."},
"base": {"type": "string", "enum": ["default_branch", "this_task"]},
"model": {"type": "string", "description": "Model for the new agent. Defaults to this session's model."}
},
"required": ["title", "prompt", "base"]
}` ),
Handler : func ( ctx context . Context , raw json . RawMessage ) ( string , error ) {
return m . createTaskFromAgent ( ctx , l , raw )
},
}
}
func ( m * Manager ) createTaskFromAgent ( ctx context . Context , l * live , raw json . RawMessage ) ( string , error ) {
args := createTaskArgs {}
err := json . Unmarshal ( raw , & args )
if err != nil {
return "" , fmt . Errorf ( "malformed create_task call: %w" , err )
}
if strings . TrimSpace ( args . Title ) == "" || strings . TrimSpace ( args . Prompt ) == "" {
return "" , errors . New ( "title and prompt are required" )
}
sess , err := m . store . GetSession ( ctx , l . id )
if err != nil {
return "" , err
}
project , err := m . store . GetProject ( ctx , l . projectID )
if err != nil {
return "" , err
}
creator , err := m . store . GetUser ( ctx , sess . CreatedBy )
if err != nil {
return "" , err
}
base := ""
switch args . Base {
case "default_branch" :
base = project . DefaultBranch
case "this_task" :
base , err = git . Head ( l . dir )
if err != nil {
return "" , err
}
default :
return "" , fmt . Errorf ( "base must be default_branch or this_task, not %q" , args . Base )
}
model := args . Model
if model == "" {
model = sess . Model
}
task , err := m . CreateTask ( ctx , project , args . Title , creator . Login , base )
if err != nil {
return "" , err
}
2026-10-14 20:07:26 -07:00
_ , err = m . create ( ctx , task , creator , args . Prompt , model , agentActor , nil )
2026-10-14 19:35:34 -07:00
if err != nil {
return "" , fmt . Errorf ( "task %s created, but its agent did not start: %w" , task . Id , err )
}
return fmt . Sprintf ( "Task %s %q created from %s and its agent started." , task . Id , task . Title , task . BaseRef ), nil
}