2026-10-18 20:30:51 -07:00
package agents
import (
"context"
"encoding/json"
"errors"
"fmt"
2026-10-20 11:01:59 -07:00
"log/slog"
2026-10-18 20:30:51 -07:00
"g.fc.run/ai/byre/internal/api"
"g.fc.run/ai/byre/internal/git"
"g.fc.run/ai/byre/internal/mcp"
2026-10-20 11:01:59 -07:00
"g.fc.run/ai/byre/internal/store"
2026-10-18 20:30:51 -07:00
)
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
}
2026-10-20 11:01:59 -07:00
login := sess . CreatedBy
2026-10-21 13:59:37 -07:00
by , err := m . store . LandingBy ( ctx , l . taskID , l . id )
if err != nil {
return "" , err
}
if by != "" {
2026-10-20 11:01:59 -07:00
login = by
}
user , err := m . store . GetUser ( ctx , login )
2026-10-18 20:30:51 -07:00
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
}
2026-10-21 11:28:07 -07:00
holder , err := m . store . ClaimMainPush ( ctx , task . Id )
if err != nil {
return "" , err
2026-10-18 20:30:51 -07:00
}
if holder != "" {
return "" , fmt . Errorf ( "%w: task %s is merging into %s; call again once it is done" , ErrConflict , holder , project . DefaultBranch )
}
2026-10-21 11:28:07 -07:00
defer m . endMainPush ( task . Id )
2026-10-18 20:30:51 -07:00
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
}
2026-10-21 12:13:04 -07:00
task . MergeTitle = title
commit , notes , err := m . prepareMainCommit ( ctx , task , project , user )
2026-10-18 20:30:51 -07:00
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
}
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
2026-10-21 12:13:04 -07:00
err = advanceMainCheckout ( project , commit , notes )
2026-10-18 20:30:51 -07:00
if err != nil {
2026-10-21 12:13:04 -07:00
return "" , err
2026-10-18 20:30:51 -07:00
}
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
}
2026-10-20 11:01:59 -07:00
2026-10-21 11:28:07 -07:00
func ( m * Manager ) endMainPush ( taskID string ) {
err := m . store . EndMainPush ( context . Background (), taskID )
if err != nil {
slog . Error ( "end push to main" , "task" , taskID , "err" , err )
}
m . poke ()
}
2026-10-20 11:01:59 -07:00
func landPrompt ( target , title string ) string {
return fmt . Sprintf ( "The human asked to merge this task's work so far into %[1]s and keep the task open (Merge & continue); byre holds the task's other agents until this turn ends. " +
"First merge %[1]s in with `git merge --no-ff --no-commit %[1]s`, resolve any conflicts keeping both sides' intent, regenerate generated files instead of hand-merging them, and check that the build and tests pass. " +
"If resolving the conflicts took substantive changes rather than combining both sides as written, stop there: end your turn with a description of those changes, so the review covers them before anything lands. " +
"Otherwise call merge_to_main with title %[2]q, which commits the merge and lands the work on %[1]s, then end your turn with one line." , target , title )
}
func ( m * Manager ) MergeToMain ( ctx context . Context , task api . Task , project api . Project , user api . User , title string ) ( api . Task , error ) {
if user . GitName == "" || user . GitEmail == "" {
return api . Task {}, ErrNoGitIdentity
}
title , err := cleanMergeTitle ( title )
if err != nil {
return api . Task {}, err
}
if task . Merging || task . MergeQueuedAt != nil || task . MergingIntoParent || task . Discarding {
return api . Task {}, fmt . Errorf ( "%w: task %s is %s" , ErrConflict , task . Id , task . State )
}
sess , found , err := m . liveSession ( ctx , task . Id )
if err != nil {
return api . Task {}, err
}
if ! found {
return api . Task {}, fmt . Errorf ( "%w: task %s has no agent" , ErrConflict , task . Id )
}
unlock := m . lockDelivery ( sess . Id )
defer unlock ()
2026-10-21 13:59:37 -07:00
err = m . store . StartLanding ( ctx , task . Id , sess . Id , user . Login )
if errors . Is ( err , store . ErrStateChanged ) {
return api . Task {}, fmt . Errorf ( "%w: %w" , ErrConflict , err )
2026-10-20 11:01:59 -07:00
}
if err != nil {
2026-10-21 13:59:37 -07:00
return api . Task {}, err
}
m . clearMergeAfterReview ( ctx , task . Id )
_ , err = m . launch ( ctx , sess , task , "byre" , landPrompt ( project . DefaultBranch , title ), "" , "" , nil )
if err != nil {
2026-10-20 11:01:59 -07:00
return api . Task {}, err
}
2026-10-20 15:01:26 -07:00
err = m . store . ClearReady ( ctx , task . Id , user . Login , "merge" )
2026-10-20 11:01:59 -07:00
if err != nil && ! errors . Is ( err , store . ErrNotFound ) {
return api . Task {}, err
}
return m . store . GetTask ( ctx , task . Id )
}
2026-10-21 13:59:37 -07:00
func ( m * Manager ) deliverQueuedTurn ( sessionID string ) {
err := m . Deliver ( context . Background (), sessionID )
2026-10-20 11:01:59 -07:00
if err != nil {
2026-10-21 13:59:37 -07:00
slog . Error ( "deliver queued message" , "session" , sessionID , "err" , err )
2026-10-20 11:01:59 -07:00
}
}