Add Merge & continue and Other… to ready cards

This commit is contained in:
Ian Gulliver
2026-10-20 11:01:59 -07:00
parent 25f275e012
commit ef98b1179d
13 changed files with 363 additions and 88 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ func (m *Manager) Compact(ctx context.Context, sessionID, model string) (api.Age
if sess.Owner != "" {
prompt = leadHandoffPrompt
}
err = transition(ctx, m.store, sess.Id, api.AgentSessionStateIdle, api.AgentSessionStateRunning, "byre")
err = m.startRunning(ctx, sess, task.Id, "byre")
if err != nil {
return api.AgentSession{}, err
}
+29 -6
View File
@@ -63,6 +63,7 @@ type Manager struct {
finishing map[string]bool
handoffModel map[string]string
mergeTurns map[string]mergeTurn
landing map[string]landing
deliverMu sync.Mutex
deliverLocks map[string]*sessionLock
@@ -158,6 +159,7 @@ func New(st *store.Store, worktreeRoot, leadRoot, handsRoot, codexHome, antigrav
finishing: map[string]bool{},
handoffModel: map[string]string{},
mergeTurns: map[string]mergeTurn{},
landing: map[string]landing{},
signals: make(chan []api.Event, 1024),
snaps: map[string]*snapState{},
merges: map[string]*projectMerge{},
@@ -537,13 +539,32 @@ func (m *Manager) Usage(ctx context.Context) (Usage, error) {
}
func (m *Manager) turn(ctx context.Context, sess api.AgentSession, task api.Task, actor, text string, images []string) (api.AgentSession, error) {
err := transition(ctx, m.store, sess.Id, api.AgentSessionStateIdle, api.AgentSessionStateRunning, actor)
err := m.startRunning(ctx, sess, task.Id, actor)
if err != nil {
return api.AgentSession{}, err
}
return m.launch(ctx, sess, task, actor, text, images)
}
func (m *Manager) startRunning(ctx context.Context, sess api.AgentSession, taskID, actor string) error {
held := fmt.Errorf("%w: held while this task merges and continues", ErrConflict)
if m.heldForLanding(taskID, sess.Id) {
return held
}
err := transition(ctx, m.store, sess.Id, api.AgentSessionStateIdle, api.AgentSessionStateRunning, actor)
if err != nil {
return err
}
if m.heldForLanding(taskID, sess.Id) {
err = transition(ctx, m.store, sess.Id, api.AgentSessionStateRunning, api.AgentSessionStateIdle, actor)
if err != nil {
return err
}
return held
}
return nil
}
func (m *Manager) launch(ctx context.Context, sess api.AgentSession, task api.Task, actor, text string, images []string) (api.AgentSession, error) {
l, err := m.ensureLive(ctx, sess, task)
if err == nil {
@@ -1070,6 +1091,8 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
return m.reviewTurnEnded(ctx, l, res)
}
defer m.endLanding(l.taskID, l.id)
m.mu.Lock()
interrupted := l.interrupted
rateLimited := l.rateLimited
@@ -1091,11 +1114,11 @@ func (m *Manager) turnEnded(ctx context.Context, l *live, res endResult) error {
m.endMergeTurn(l.id, false)
}
}()
if !finished {
err := git.AbortMerge(l.dir)
if err != nil {
slog.Error("abort unfinished merge", "session", l.id, "err", err)
}
}
if (owner || m.landingBy(l.taskID, l.id) != "") && !finished {
err := git.AbortMerge(l.dir)
if err != nil {
slog.Error("abort unfinished merge", "session", l.id, "err", err)
}
}
unlock := func() {}
+120 -1
View File
@@ -5,10 +5,12 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"g.fc.run/ai/byre/internal/api"
"g.fc.run/ai/byre/internal/git"
"g.fc.run/ai/byre/internal/mcp"
"g.fc.run/ai/byre/internal/store"
)
type mergeToMainArgs struct {
@@ -52,7 +54,11 @@ func (m *Manager) mergeToMain(ctx context.Context, l *live, raw json.RawMessage)
if err != nil {
return "", err
}
user, err := m.store.GetUser(ctx, sess.CreatedBy)
login := sess.CreatedBy
if by := m.landingBy(l.taskID, l.id); by != "" {
login = by
}
user, err := m.store.GetUser(ctx, login)
if err != nil {
return "", err
}
@@ -168,3 +174,116 @@ func (m *Manager) pushToMain(ctx context.Context, l *live, task api.Task, projec
}
return commit, nil
}
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()
m.mu.Lock()
holder := m.landing[task.Id]
if holder.session == "" {
m.landing[task.Id] = landing{session: sess.Id, login: user.Login}
}
m.mu.Unlock()
if holder.session != "" {
return api.Task{}, fmt.Errorf("%w: task %s is already merging and continuing", ErrConflict, task.Id)
}
sessions, err := m.store.ListSessions(ctx, task.Id)
if err == nil {
for _, s := range sessions {
if s.Role == api.Implementer && s.State == api.AgentSessionStateRunning {
err = fmt.Errorf("%w: an agent on this task is mid-turn; merge when they're all idle", ErrConflict)
}
}
}
if err == nil {
m.clearMergeAfterReview(ctx, task.Id)
_, err = m.Send(ctx, sess.Id, "byre", landPrompt(project.DefaultBranch, title), "")
}
if err != nil {
m.endLanding(task.Id, sess.Id)
return api.Task{}, err
}
err = m.store.ClearReady(ctx, task.Id, user.Login)
if err != nil && !errors.Is(err, store.ErrNotFound) {
return api.Task{}, err
}
return m.store.GetTask(ctx, task.Id)
}
type landing struct {
session string
login string
}
func (m *Manager) heldForLanding(taskID, sessionID string) bool {
m.mu.Lock()
defer m.mu.Unlock()
holder := m.landing[taskID].session
return holder != "" && holder != sessionID
}
func (m *Manager) landingBy(taskID, sessionID string) string {
m.mu.Lock()
defer m.mu.Unlock()
if m.landing[taskID].session != sessionID {
return ""
}
return m.landing[taskID].login
}
func (m *Manager) endLanding(taskID, sessionID string) {
m.mu.Lock()
held := m.landing[taskID].session == sessionID
if held {
delete(m.landing, taskID)
}
m.mu.Unlock()
if held {
go m.releaseTeam(taskID, sessionID)
}
}
func (m *Manager) releaseTeam(taskID, landingID string) {
ctx := context.Background()
sessions, err := m.store.ListSessions(ctx, taskID)
if err != nil {
slog.Error("list sessions after merge and continue", "task", taskID, "err", err)
return
}
for _, s := range sessions {
if s.Id == landingID || s.Role != api.Implementer || s.State != api.AgentSessionStateIdle || m.interrupted(s.Id) {
continue
}
err = m.Deliver(ctx, s.Id)
if err != nil {
slog.Error("deliver after merge and continue", "session", s.Id, "err", err)
}
}
}
+1 -1
View File
@@ -53,7 +53,7 @@ func (m *Manager) Message(ctx context.Context, sessionID, actor, text, model str
return api.AgentSession{}, false, err
}
}
held := sess.State == api.AgentSessionStateIdle && sess.Role == api.Implementer && automatedActor(actor) && m.interrupted(sess.Id)
held := sess.State == api.AgentSessionStateIdle && sess.Role == api.Implementer && (automatedActor(actor) && m.interrupted(sess.Id) || m.heldForLanding(sess.TaskId, sess.Id))
if !held && (sess.State != api.AgentSessionStateRunning || sess.Role != api.Implementer) {
sess, err = m.Send(ctx, sessionID, actor, text, model, images...)
return sess, false, err
+11 -3
View File
@@ -26,6 +26,7 @@ const readyPromptUnchanged = "Your turn has ended, and this task's branch has no
type readyArgs struct {
Overview string `json:"overview"`
Merge string `json:"merge"`
}
func (m *Manager) readyForMergeTool(l *live) mcp.Tool {
@@ -33,12 +34,13 @@ func (m *Manager) readyForMergeTool(l *live) mcp.Tool {
Name: "ready_for_merge",
Description: "Tell the human this task is ready to merge, with an overview of the whole task: all of the branch's work, not just what changed since your last call or the last discussion. " +
"Write it for someone jumping between tasks who hasn't followed this one: a summary of what the change does, the tradeoffs and compromises made, what was not done, and follow-up work for later. " +
"byre shows it in the task's transcript and passes it to the human's lead agent. " +
"byre shows it in the task's transcript, with buttons to merge and complete the task or merge and continue it, and passes it to the human's lead agent. " +
"Call it only once the review of your most recent changes has come back: byre reviews the work after each turn that changes files and messages you when that review finishes, so never call it in the same turn as a change, or while a review of your latest work is pending.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"overview": {"type": "string", "description": "Markdown, with sections for the summary, tradeoffs and compromises, not done, and future TODOs; leave out a section with nothing in it."}
"overview": {"type": "string", "description": "Markdown, with sections for the summary, tradeoffs and compromises, not done, and future TODOs; leave out a section with nothing in it."},
"merge": {"type": "string", "enum": ["complete", "continue"], "description": "The button byre highlights: complete (the default) lands the work and closes the task; continue lands the work so far and keeps the task open, for when you or the human mean to do more on it."}
},
"required": ["overview"]
}`),
@@ -52,7 +54,13 @@ func (m *Manager) readyForMergeTool(l *live) mcp.Tool {
if overview == "" {
return "", fmt.Errorf("%w: overview must not be empty", ErrInvalid)
}
err = m.store.MarkReady(ctx, l.taskID, overview, agentActor)
if args.Merge == "" {
args.Merge = "complete"
}
if args.Merge != "complete" && args.Merge != "continue" {
return "", fmt.Errorf("%w: merge must be complete or continue", ErrInvalid)
}
err = m.store.MarkReady(ctx, l.taskID, overview, args.Merge, agentActor)
if errors.Is(err, store.ErrNotFound) {
return "", fmt.Errorf("%w: this task is closed", ErrConflict)
}
+51 -45
View File
@@ -1683,6 +1683,9 @@ type MergeTaskJSONBody struct {
// AfterReview Merge once the running review of the latest snapshot finishes with no findings, instead of now.
AfterReview *bool `json:"after_review,omitempty"`
// KeepOpen Land the work so far and keep the task open, instead of closing it.
KeepOpen *bool `json:"keep_open,omitempty"`
// MergeTitle The squash commit's title: one line, at most 72 characters. Required for target main.
MergeTitle *string `json:"merge_title,omitempty"`
@@ -4376,52 +4379,55 @@ var swaggerSpec = []string{
"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",
}
// decodeSpec returns the embedded OpenAPI spec as raw JSON bytes,
+17
View File
@@ -504,6 +504,23 @@ func (s *Server) MergeTask(w http.ResponseWriter, r *http.Request, taskID string
writeError(w, http.StatusBadRequest, fmt.Errorf("unknown target %q", *req.Target))
return
}
if req.KeepOpen != nil && *req.KeepOpen {
if (req.Target != nil && *req.Target != api.MergeTargetMain) || (req.AfterReview != nil && *req.AfterReview) {
writeError(w, http.StatusBadRequest, errors.New("keep_open is only for target main, without after_review"))
return
}
if req.MergeTitle == nil {
writeError(w, http.StatusBadRequest, errors.New("merge_title is required to merge into the default branch"))
return
}
t, err = s.agents.MergeToMain(r.Context(), t, p, u, *req.MergeTitle)
if err != nil {
writeAgentError(w, err)
return
}
writeJSON(w, http.StatusAccepted, t)
return
}
if req.AfterReview != nil && *req.AfterReview {
target := api.MergeTargetMain
if req.Target != nil {
+2 -2
View File
@@ -781,8 +781,8 @@ func (s *Store) SetReadyPromptSeq(ctx context.Context, id string, seq int64) err
return err
}
func (s *Store) MarkReady(ctx context.Context, id, overview, actor string) error {
return s.setReady(ctx, id, actor, "merge", "task.ready", map[string]any{"id": id, "overview": overview})
func (s *Store) MarkReady(ctx context.Context, id, overview, merge, actor string) error {
return s.setReady(ctx, id, actor, "merge", "task.ready", map[string]any{"id": id, "overview": overview, "merge": merge})
}
func (s *Store) SuggestDiscard(ctx context.Context, id, reason, actor string) error {