Merge set_task_title and set_merge_title into one set_titles tool

This commit is contained in:
Ian Gulliver
2026-10-21 11:34:13 -07:00
parent 3f4e4d5e90
commit 5dd89d9d14
6 changed files with 366 additions and 388 deletions
+3 -3
View File
@@ -102,9 +102,9 @@ func (m *Manager) taskGuidance(ctx context.Context, task api.Task) (string, erro
"\n- This worktree starts with only what git tracks. Before other work in a new worktree, do any setup the project's AGENTS.md describes.",
task.PortBase, task.PortBase+store.PortBlock-1)
if task.Title == "" {
g += "\n- This task has no title yet. First thing, before any other work, call set_task_title with a very short, distinct, to-the-point title (two to four words), and call it again whenever the work moves on."
g += "\n- This task has no title yet. First thing, before any other work, call set_titles with a very short, distinct, to-the-point task title (two to four words) and a merge commit title that describes the work, and call it again whenever the work moves on."
} else {
g += fmt.Sprintf("\n- This task's title is %q. First thing, if it isn't very short (two to four words), distinct and to the point, call set_task_title with one that is, and call it again whenever the work moves on.", task.Title)
g += fmt.Sprintf("\n- This task's title is %q. First thing, call set_titles with a merge commit title that describes the work, and a task title that is very short (two to four words), distinct and to the point (this one, if it already is), and call it again whenever the work moves on.", task.Title)
}
if task.TodoItem != nil {
g += fmt.Sprintf("\n- This task was started from the item %q in %s at the repo root. When the work is done, remove that item from %s in this worktree, so the removal lands in the same merge as the work.",
@@ -132,7 +132,7 @@ func longTitleGuidance(task api.Task) string {
if n <= maxMergeTitle {
return ""
}
return fmt.Sprintf("\n- This task's merge commit title, %q, is %d characters, over the %d allowed, so Merge will refuse it. Early in your first turn, call set_merge_title with a one-line title of at most %d characters that describes the work.",
return fmt.Sprintf("\n- This task's merge commit title, %q, is %d characters, over the %d allowed, so Merge will refuse it. Early in your first turn, call set_titles with a one-line merge title of at most %d characters that describes the work.",
task.MergeTitle, n, maxMergeTitle, maxMergeTitle)
}
+1 -1
View File
@@ -855,7 +855,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.setTaskTitleTool(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), m.bashTool(l), m.readKnowledgeTool()}, append(append(m.fileTools(l), m.subtaskTools(l)...), m.clientTools(l)...)...)
tools := append([]mcp.Tool{m.askTool(l), m.cancelQuestionTool(l), m.setTitlesTool(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), m.bashTool(l), m.readKnowledgeTool()}, append(append(m.fileTools(l), m.subtaskTools(l)...), m.clientTools(l)...)...)
lead := true
if sess.Role == api.Implementer {
g += longTitleGuidance(task)
+26 -52
View File
@@ -33,7 +33,7 @@ func mergeInPrompt(target string) string {
}
func (m *Manager) MergeIn(ctx context.Context, task api.Task, user api.User, target string) (api.AgentSession, error) {
prompt := mergeInPrompt(target) + fmt.Sprintf(" Finally, if the merge commit title (currently \"%s\") no longer describes all the work on this branch, set a better one with set_merge_title.", task.MergeTitle)
prompt := mergeInPrompt(target) + fmt.Sprintf(" Finally, if the merge commit title (currently \"%s\") no longer describes all the work on this branch, set a better one with set_titles.", task.MergeTitle)
return m.sendMergeIn(ctx, task, user, prompt)
}
@@ -456,81 +456,55 @@ func (m *Manager) RemoveTask(ctx context.Context, task api.Task, project api.Pro
return git.RemoveWorktree(project.RepoPath, task.WorktreePath, task.Branch)
}
type setMergeTitleArgs struct {
Title string `json:"title"`
type setTitlesArgs struct {
Title string `json:"title"`
MergeTitle string `json:"merge_title"`
}
func (m *Manager) setMergeTitleTool(l *live) mcp.Tool {
func (m *Manager) setTitlesTool(l *live) mcp.Tool {
return mcp.Tool{
Name: "set_merge_title",
Description: "Set the title of the single commit this task becomes when it is merged onto the default branch. " +
"It starts as the task title. Write one line in the imperative mood that describes all the work on the branch, " +
"for example \"Add retry with backoff to webhook delivery\". It replaces any earlier title.",
Name: "set_titles",
Description: "Set this task's two titles. title names the task in byre's task list and switcher: very short (two to four words), distinct from the project's other tasks, and to the point, for example \"webhook retries\". " +
"merge_title is the title of the single commit this task becomes when it is merged onto the default branch: one line in the imperative mood that describes all the work on the branch, " +
"for example \"Add retry with backoff to webhook delivery\". Each replaces the earlier one.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"title": {"type": "string", "description": "One line, at most 72 characters."}
"title": {"type": "string", "description": "The task title: one short line."},
"merge_title": {"type": "string", "description": "The merge commit title: one line, at most 72 characters."}
},
"required": ["title"]
"required": ["title", "merge_title"]
}`),
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
return m.setMergeTitle(ctx, l, raw)
return m.setTitles(ctx, l, raw)
},
}
}
func (m *Manager) setMergeTitle(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
args := setMergeTitleArgs{}
func (m *Manager) setTitles(ctx context.Context, l *live, raw json.RawMessage) (string, error) {
args := setTitlesArgs{}
err := json.Unmarshal(raw, &args)
if err != nil {
return "", fmt.Errorf("malformed set_merge_title call: %w", err)
return "", fmt.Errorf("malformed set_titles call: %w", err)
}
title, err := cleanMergeTitle(args.Title)
title := strings.TrimSpace(args.Title)
if title == "" || strings.ContainsAny(title, "\r\n") {
return "", fmt.Errorf("%w: title must be one non-empty line", ErrInvalid)
}
mergeTitle, err := cleanMergeTitle(args.MergeTitle)
if err != nil {
return "", err
}
err = m.storeMergeTitle(ctx, l.taskID, title, agentActor)
err = m.store.SetTitles(ctx, l.taskID, title, mergeTitle, agentActor)
if errors.Is(err, store.ErrNotFound) {
return "", fmt.Errorf("%w: this task is closed, or its merge was already confirmed with its title", ErrConflict)
}
if err != nil {
return "", err
}
return fmt.Sprintf("Merge commit title set to %q.", title), nil
}
func (m *Manager) setTaskTitleTool(l *live) mcp.Tool {
return mcp.Tool{
Name: "set_task_title",
Description: "Set this task's title, which names it in byre's task list and switcher. " +
"Keep it very short (two to four words), distinct from the project's other tasks, and to the point, for example \"webhook retries\". " +
"Until set_merge_title is called, the merge commit title follows it. It replaces any earlier title.",
InputSchema: json.RawMessage(`{
"type": "object",
"properties": {
"title": {"type": "string", "description": "One short line."}
},
"required": ["title"]
}`),
Handler: func(ctx context.Context, raw json.RawMessage) (string, error) {
args := setMergeTitleArgs{}
err := json.Unmarshal(raw, &args)
if err != nil {
return "", fmt.Errorf("malformed set_task_title call: %w", err)
}
title := strings.TrimSpace(args.Title)
if title == "" || strings.ContainsAny(title, "\r\n") {
return "", fmt.Errorf("%w: title must be one non-empty line", ErrInvalid)
}
err = m.store.SetTitle(ctx, l.taskID, title, agentActor)
if errors.Is(err, store.ErrNotFound) {
return "", fmt.Errorf("%w: this task is closed or merging", ErrConflict)
}
if err != nil {
return "", err
}
return fmt.Sprintf("Task title set to %q.", title), nil
},
}
return fmt.Sprintf("Task title set to %q, merge commit title to %q.", title, mergeTitle), nil
}
type stopMergeArgs struct {