Let the agent stop a merge for review after substantive conflict fixes

This commit is contained in:
Ian Gulliver
2026-10-15 19:36:42 -07:00
parent a2067c2ba0
commit 55b6a32263
7 changed files with 59 additions and 8 deletions
+50 -2
View File
@@ -21,7 +21,9 @@ const maxMergeTitle = 72
func mergeInPrompt(target string) string {
return fmt.Sprintf("Merge %[1]s into this branch: run `git merge --no-ff --no-commit %[1]s`. "+
"Resolve any conflicts keeping both sides' intent; regenerate generated files instead of hand-merging them. "+
"Check that the build and tests pass. Leave the merge uncommitted: byre concludes it when the turn ends.", target)
"Check that the build and tests pass. Leave the merge uncommitted: byre concludes it when the turn ends. "+
"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 of the turn's snapshot covers them.", target)
}
func (m *Manager) MergeIn(ctx context.Context, task api.Task, user api.User, target string) (api.AgentSession, error) {
@@ -100,7 +102,8 @@ func (m *Manager) startMerge(ctx context.Context, task api.Task, project api.Pro
}
m.mergeHolders[project.Id] = task.Id
_, err = m.sendMergeIn(ctx, task, user, mergeInPrompt(project.DefaultBranch))
prompt := mergeInPrompt(project.DefaultBranch) + fmt.Sprintf(" When this turn ends, byre squashes this branch onto %s and pushes it; if you stop, call stop_merge first so it doesn't.", project.DefaultBranch)
_, err = m.sendMergeIn(ctx, task, user, prompt)
if err != nil {
m.takeMerge(task.Id)
delete(m.mergeHolders, project.Id)
@@ -395,6 +398,51 @@ func (m *Manager) setMergeTitle(ctx context.Context, l *live, raw json.RawMessag
return fmt.Sprintf("Merge commit title set to %q.", title), nil
}
type stopMergeArgs struct {
Reason string `json:"reason"`
}
func (m *Manager) stopMergeTool(l *live) mcp.Tool {
return mcp.Tool{
Name: "stop_merge",
Description: "Stop the merge byre is to make when this turn ends, so the task stays active and its snapshot is reviewed before anything is pushed. " +
"Call it during a Merge turn when resolving conflicts took substantive changes, then end your turn describing them. The human merges again after the review.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"reason": {"type": "string", "description": "The substantive changes the resolution made, in a sentence or two."}
},
"required": ["reason"]
}`),
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
return m.stopMerge(ctx, l, raw)
},
}
}
func (m *Manager) stopMerge(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
args := stopMergeArgs{}
err := json.Unmarshal(raw, &args)
if err != nil {
return "", fmt.Errorf("malformed stop_merge call: %w", err)
}
reason := strings.TrimSpace(args.Reason)
if reason == "" {
return "", fmt.Errorf("%w: reason must say what the resolution changed", ErrInvalid)
}
if m.takeMerge(l.taskID) == "" {
return "No merge is pending for this task; just end your turn.", nil
}
go m.releaseMerge(l.projectID, l.taskID)
err = m.emit(ctx, l, agentActor, "task.merge_stopped", map[string]any{"id": l.taskID, "reason": reason})
if err != nil {
return "", err
}
return "Merge stopped: byre won't squash or push when this turn ends. End your turn now with a description of the changes.", nil
}
func cleanMergeTitle(raw string) (string, error) {
title := strings.TrimSpace(raw)
if title == "" || strings.ContainsAny(title, "\r\n") {