package agents import ( "context" "errors" "fmt" "log/slog" "path/filepath" "time" "g.fc.run/misc/byre/internal/api" "g.fc.run/misc/byre/internal/git" "g.fc.run/misc/byre/internal/store" "g.fc.run/misc/byre/internal/todo" ) 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) } 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 } _, err = m.create(ctx, task, creator, prompt, 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 (m *Manager) StartTodo(ctx context.Context, project api.Project, creator api.User, title, model, actor, workflowName string, discuss bool) ([]api.Task, error) { if creator.GitName == "" || creator.GitEmail == "" { return nil, ErrNoGitIdentity } src, found, err := git.ReadFile(project.RepoPath, project.DefaultBranch, todo.File) if err != nil { return nil, err } if !found { return nil, fmt.Errorf("%w: %s has no %s", store.ErrNotFound, project.DefaultBranch, todo.File) } item, found := todo.Find(todo.Parse(src), title) if !found { return nil, 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 nil, err } for _, t := range tasks { if t.Status == api.TaskStatusActive && t.TodoItem != nil && *t.TodoItem == title { return nil, fmt.Errorf("%w: task %s is already working on %q", ErrConflict, t.Id, title) } } started := []api.Task{} if workflowName != "" { started, err = m.StartWorkflow(ctx, project, creator, item.Title, project.DefaultBranch, actor, workflowName, todoPrompt(item, discuss), &item.Title) if err != nil { 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) if err != nil { return nil, err } _, err = m.create(ctx, task, creator, todoPrompt(item, discuss), model, actor, nil) if err != nil { return nil, fmt.Errorf("task %s created, but its agent did not start: %w", task.Id, err) } started = append(started, task) } if actor == creator.Login { m.setStartedTodo(creator.Login, true) } return started, nil } 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.", todo.File, todo.Markdown(item)) if !discuss { return p } 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." } func (m *Manager) createTask(ctx context.Context, project api.Project, t api.Task, frozen *store.FrozenWorkflow, base, actor string) (api.Task, error) { t.Id = store.NewID() t.ProjectId = project.Id if t.MergeTitle == "" { t.MergeTitle = t.Title } t.Branch = "byre/" + t.Id t.WorktreePath = filepath.Join(m.worktreeRoot, project.Id, t.Id) t.Status = api.TaskStatusActive t.CreatedAt = time.Now().UTC() 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, frozen, actor) if err != nil { 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) } return api.Task{}, err } return m.store.GetTask(ctx, t.Id) }