Add merge_to_main tool to land work mid-task and keep going
This commit is contained in:
@@ -738,7 +738,7 @@ func (m *Manager) startLive(ctx context.Context, sess api.AgentSession, task api
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setMergeTitleTool(l), m.stopMergeTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.takeSnapshotTool(l), m.terminalReadTool(l), m.terminalTypeTool(l)}, append(m.subtaskTools(l), m.clientTools(l)...)...)
|
||||
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setMergeTitleTool(l), m.stopMergeTool(l), m.mergeToMainTool(l), m.setTaskSummaryTool(l), m.searchClosedTool(l), m.exposePortTool(l), m.takeSnapshotTool(l), m.terminalReadTool(l), m.terminalTypeTool(l)}, append(m.subtaskTools(l), m.clientTools(l)...)...)
|
||||
lead := true
|
||||
if sess.Role == api.Implementer {
|
||||
g += longTitleGuidance(task)
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package agents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"g.fc.run/ai/byre/internal/api"
|
||||
"g.fc.run/ai/byre/internal/git"
|
||||
"g.fc.run/ai/byre/internal/mcp"
|
||||
)
|
||||
|
||||
type mergeToMainArgs struct {
|
||||
Title string `json:"title"`
|
||||
}
|
||||
|
||||
func (m *Manager) mergeToMainTool(l *live) mcp.Tool {
|
||||
return mcp.Tool{
|
||||
Name: "merge_to_main",
|
||||
Description: "Land this task's work so far on the default branch now and keep working: byre commits the worktree, squashes the branch onto the default branch as one commit titled title, pushes it, " +
|
||||
"and merges that commit back into this branch, so the task stays open and its later Merge carries only what comes after. " +
|
||||
"Refused when the branch conflicts with the default branch: merge the default branch in, resolve the conflicts, and call it again.",
|
||||
InputSchema: json.RawMessage(`{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {"type": "string", "description": "The commit's title: one line in the imperative mood, at most 72 characters, describing the work landing now."}
|
||||
},
|
||||
"required": ["title"]
|
||||
}`),
|
||||
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
|
||||
return m.mergeToMain(ctx, l, raw)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) mergeToMain(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
|
||||
args := mergeToMainArgs{}
|
||||
err := json.Unmarshal(raw, &args)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("malformed merge_to_main call: %w", err)
|
||||
}
|
||||
title, err := cleanMergeTitle(args.Title)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if m.ownsTask(l.id) {
|
||||
return "", fmt.Errorf("%w: this turn is merging into the worktree; end it first", ErrConflict)
|
||||
}
|
||||
|
||||
sess, err := m.store.GetSession(ctx, l.id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
user, err := m.store.GetUser(ctx, sess.CreatedBy)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if user.GitName == "" || user.GitEmail == "" {
|
||||
return "", ErrNoGitIdentity
|
||||
}
|
||||
|
||||
unlock := m.lockTask(l.taskID)
|
||||
defer unlock()
|
||||
|
||||
task, err := m.store.GetTask(ctx, l.taskID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if task.Status != api.TaskStatusActive || task.Merging || task.MergeQueuedAt != nil || task.MergingIntoParent || task.Discarding {
|
||||
return "", fmt.Errorf("%w: task %s is %s", ErrConflict, task.Id, task.State)
|
||||
}
|
||||
project, err := m.store.GetProject(ctx, task.ProjectId)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pm := m.projectMerge(project.Id)
|
||||
pm.mu.Lock()
|
||||
holder := pm.holder
|
||||
if holder == "" {
|
||||
pm.holder = task.Id
|
||||
}
|
||||
pm.mu.Unlock()
|
||||
if holder != "" {
|
||||
return "", fmt.Errorf("%w: task %s is merging into %s; call again once it is done", ErrConflict, holder, project.DefaultBranch)
|
||||
}
|
||||
defer m.releaseMerge(project.Id, task.Id)
|
||||
|
||||
commit, err := m.pushToMain(ctx, l, task, project, user, title)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if commit == "" {
|
||||
return fmt.Sprintf("Nothing to push: %s already has all of this branch's work, so byre merged %s into this branch. Keep working; the task stays open.", project.DefaultBranch, project.DefaultBranch), nil
|
||||
}
|
||||
|
||||
pushed := git.Remote + "/" + project.DefaultBranch
|
||||
err = m.emit(ctx, l, agentActor, "task.merged_to_main", map[string]any{"id": task.Id, "commit": commit, "pushed": pushed, "merge_title": title})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("Pushed %s %q to %s and merged it back into this branch. Keep working; the task stays open.", commit, title, pushed), nil
|
||||
}
|
||||
|
||||
func (m *Manager) pushToMain(ctx context.Context, l *live, task api.Task, project api.Project, user api.User, title string) (string, error) {
|
||||
err := git.FetchFastForward(ctx, project.RepoPath, project.DefaultBranch)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = git.CommitAll(l.dir, title, l.gitName, l.gitEmail)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
coAuthors, err := m.CoAuthors(ctx, task.Id, user.Login)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
env := []string{
|
||||
"GIT_AUTHOR_NAME=" + user.GitName,
|
||||
"GIT_AUTHOR_EMAIL=" + user.GitEmail,
|
||||
"GIT_COMMITTER_NAME=" + user.GitName,
|
||||
"GIT_COMMITTER_EMAIL=" + user.GitEmail,
|
||||
}
|
||||
commit, err := git.SquashCommit(project.RepoPath, l.dir, project.DefaultBranch, squashMessage(title, coAuthors), env)
|
||||
conflict := &git.ConflictError{}
|
||||
if errors.As(err, &conflict) {
|
||||
return "", fmt.Errorf("%w; run `git merge --no-ff --no-commit %s`, resolve them, and call merge_to_main again", err, project.DefaultBranch)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if commit == "" {
|
||||
err = git.Merge(l.dir, project.DefaultBranch, "Merge "+project.DefaultBranch, l.gitName, l.gitEmail)
|
||||
return "", err
|
||||
}
|
||||
|
||||
task.MergeTitle = title
|
||||
note, err := m.mergeNote(ctx, task, user.Login)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
notes, err := git.NoteCommit(project.RepoPath, commit, note, env)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = git.Push(ctx, project.RepoPath, commit, project.DefaultBranch, notes)
|
||||
if errors.Is(err, git.ErrRemoteMoved) {
|
||||
return "", fmt.Errorf("%w; call merge_to_main again", err)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
pushed := git.Remote + "/" + project.DefaultBranch
|
||||
err = git.AdvanceNotes(project.RepoPath, notes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pushed %s to %s, but updating %s in the project checkout failed: %w", commit, pushed, git.NotesRef, err)
|
||||
}
|
||||
err = git.FastForward(project.RepoPath, commit)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pushed %s to %s, but fast-forwarding the project checkout failed: %w", commit, pushed, err)
|
||||
}
|
||||
err = git.Merge(l.dir, commit, fmt.Sprintf("Merge %s from %s", commit[:7], project.DefaultBranch), l.gitName, l.gitEmail)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("pushed %s to %s, but merging it back into this branch failed: %w", commit, pushed, err)
|
||||
}
|
||||
return commit, nil
|
||||
}
|
||||
+16
-1
@@ -329,7 +329,11 @@ func CommitAll(dir, message, name, email string) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if status == "" {
|
||||
merging, err := MergeInProgress(dir)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if status == "" && !merging {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -350,6 +354,17 @@ func CommitAll(dir, message, name, email string) (string, error) {
|
||||
return Head(dir)
|
||||
}
|
||||
|
||||
func Merge(dir, rev, message, name, email string) error {
|
||||
env := []string{
|
||||
"GIT_AUTHOR_NAME=" + name,
|
||||
"GIT_AUTHOR_EMAIL=" + email,
|
||||
"GIT_COMMITTER_NAME=" + name,
|
||||
"GIT_COMMITTER_EMAIL=" + email,
|
||||
}
|
||||
_, err := runEnv(dir, env, "merge", "--no-ff", "--quiet", "--message", message, rev)
|
||||
return err
|
||||
}
|
||||
|
||||
func run(dir string, args ...string) (string, error) {
|
||||
return runEnv(dir, nil, args...)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user