loop: split title tool into title and precommit tools

title wants to be called early, as soon as the topic is clear.
precommit wants to be called late, just before first git commit.

Co-Authored-By: sketch <hello@sketch.dev>
diff --git a/loop/agent.go b/loop/agent.go
index df76140..5b63dee 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -525,15 +525,18 @@
 	return a.firstMessageIndex
 }
 
-// SetTitleBranch sets the title and branch name of the conversation.
-func (a *Agent) SetTitleBranch(title, branchName string) {
+// SetTitle sets the title of the conversation.
+func (a *Agent) SetTitle(title string) {
 	a.mu.Lock()
 	defer a.mu.Unlock()
 	a.title = title
-	a.branchName = branchName
+}
 
-	// TODO: We could potentially notify listeners of a state change, but,
-	// realistically, a new message will be sent for the tool result as well.
+// SetBranch sets the branch name of the conversation.
+func (a *Agent) SetBranch(branchName string) {
+	a.mu.Lock()
+	defer a.mu.Unlock()
+	a.branchName = branchName
 }
 
 // OnToolCall implements ant.Listener and tracks the start of a tool call.
@@ -832,7 +835,7 @@
 
 		// If it's a git commit and branch is not set, return an error
 		if willCommit {
-			return fmt.Errorf("you must use the title tool before making git commits")
+			return fmt.Errorf("you must use the precommit tool before making git commits")
 		}
 
 		return nil
@@ -860,7 +863,7 @@
 
 	convo.Tools = []*llm.Tool{
 		bashTool, claudetool.Keyword,
-		claudetool.Think, a.preCommitTool(), makeDoneTool(a.codereview, a.config.GitUsername, a.config.GitEmail),
+		claudetool.Think, a.titleTool(), a.precommitTool(), makeDoneTool(a.codereview, a.config.GitUsername, a.config.GitEmail),
 		a.codereview.Tool(), a.multipleChoiceTool(),
 	}
 
@@ -947,12 +950,9 @@
 	return false
 }
 
-func (a *Agent) preCommitTool() *llm.Tool {
-	description := `Sets the conversation title and creates a git branch for tracking work. MANDATORY: You must use this tool before making any git commits.`
-	if experiment.Enabled("precommit") {
-		description = `Sets the conversation title, creates a git branch for tracking work, and provides git commit message style guidance. MANDATORY: You must use this tool before making any git commits.`
-	}
-	preCommit := &llm.Tool{
+func (a *Agent) titleTool() *llm.Tool {
+	description := `Sets the conversation title.`
+	titleTool := &llm.Tool{
 		Name:        "title",
 		Description: description,
 		InputSchema: json.RawMessage(`{
@@ -960,36 +960,72 @@
 	"properties": {
 		"title": {
 			"type": "string",
-			"description": "A concise title summarizing what this conversation is about, imperative tense preferred"
-		},
-		"branch_name": {
-			"type": "string",
-			"description": "A 2-3 word alphanumeric hyphenated slug for the git branch name"
+			"description": "Brief title (3-6 words) in imperative tense. Focus on core action/component."
 		}
 	},
-	"required": ["title", "branch_name"]
+	"required": ["title"]
 }`),
 		Run: func(ctx context.Context, input json.RawMessage) (string, error) {
 			var params struct {
-				Title      string `json:"title"`
-				BranchName string `json:"branch_name"`
+				Title string `json:"title"`
 			}
 			if err := json.Unmarshal(input, &params); err != nil {
 				return "", err
 			}
-			// It's unfortunate to not allow title changes,
-			// but it avoids accidentally generating multiple branches.
+
+			// We don't allow changing the title once set to be consistent with the previous behavior
+			// and to prevent accidental title changes
 			t := a.Title()
 			if t != "" {
 				return "", fmt.Errorf("title already set to: %s", t)
 			}
 
-			if params.BranchName == "" {
-				return "", fmt.Errorf("branch_name parameter cannot be empty")
-			}
 			if params.Title == "" {
 				return "", fmt.Errorf("title parameter cannot be empty")
 			}
+
+			a.SetTitle(params.Title)
+			response := fmt.Sprintf("Title set to %q", params.Title)
+			return response, nil
+		},
+	}
+	return titleTool
+}
+
+func (a *Agent) precommitTool() *llm.Tool {
+	description := `Creates a git branch for tracking work. MANDATORY: You must use this tool before making any git commits.`
+	if experiment.Enabled("precommit") {
+		description = `Creates a git branch for tracking work and provides git commit message style guidance. MANDATORY: You must use this tool before making any git commits.`
+	}
+	preCommit := &llm.Tool{
+		Name:        "precommit",
+		Description: description,
+		InputSchema: json.RawMessage(`{
+	"type": "object",
+	"properties": {
+		"branch_name": {
+			"type": "string",
+			"description": "A 2-3 word alphanumeric hyphenated slug for the git branch name"
+		}
+	},
+	"required": ["branch_name"]
+}`),
+		Run: func(ctx context.Context, input json.RawMessage) (string, error) {
+			var params struct {
+				BranchName string `json:"branch_name"`
+			}
+			if err := json.Unmarshal(input, &params); err != nil {
+				return "", err
+			}
+
+			b := a.BranchName()
+			if b != "" {
+				return "", fmt.Errorf("branch already set to: %s", b)
+			}
+
+			if params.BranchName == "" {
+				return "", fmt.Errorf("branch_name parameter cannot be empty")
+			}
 			if params.BranchName != cleanBranchName(params.BranchName) {
 				return "", fmt.Errorf("branch_name parameter must be alphanumeric hyphenated slug")
 			}
@@ -998,9 +1034,8 @@
 				return "", fmt.Errorf("branch %q already exists; please choose a different branch name", branchName)
 			}
 
-			a.SetTitleBranch(params.Title, branchName)
-
-			response := fmt.Sprintf("Title set to %q, branch name set to %q", params.Title, branchName)
+			a.SetBranch(branchName)
+			response := fmt.Sprintf("Branch name set to %q", branchName)
 
 			if experiment.Enabled("precommit") {
 				styleHint, err := claudetool.CommitMessageStyleHint(ctx, a.repoRoot)
diff --git a/loop/agent_system_prompt.txt b/loop/agent_system_prompt.txt
index b6b7de3..d7af70d 100644
--- a/loop/agent_system_prompt.txt
+++ b/loop/agent_system_prompt.txt
@@ -4,8 +4,7 @@
 Start by asking concise clarifying questions as needed.
 Once the intent is clear, work autonomously.
 
-Call the title tool early in the conversation to provide a brief summary of
-what the chat is about.
+Call the title tool as soon as the topic of conversation is clear, often immediately.
 
 Break down the overall goal into a series of smaller steps.
 (The first step is often: "Make a plan.")
diff --git a/loop/testdata/agent_loop.httprr b/loop/testdata/agent_loop.httprr
index ba483b9..867ca95 100644
--- a/loop/testdata/agent_loop.httprr
+++ b/loop/testdata/agent_loop.httprr
@@ -1,9 +1,9 @@
 httprr trace v1
-14168 2249
+14290 2209
 POST https://api.anthropic.com/v1/messages HTTP/1.1

 Host: api.anthropic.com

 User-Agent: Go-http-client/1.1

-Content-Length: 13970

+Content-Length: 14092

 Anthropic-Version: 2023-06-01

 Content-Type: application/json

 

@@ -91,21 +91,32 @@
   },
   {
    "name": "title",
-   "description": "Sets the conversation title and creates a git branch for tracking work. MANDATORY: You must use this tool before making any git commits.",
+   "description": "Sets the conversation title.",
    "input_schema": {
     "type": "object",
     "properties": {
      "title": {
       "type": "string",
-      "description": "A concise title summarizing what this conversation is about, imperative tense preferred"
-     },
+      "description": "Brief title (3-6 words) in imperative tense. Focus on core action/component."
+     }
+    },
+    "required": [
+     "title"
+    ]
+   }
+  },
+  {
+   "name": "precommit",
+   "description": "Creates a git branch for tracking work. MANDATORY: You must use this tool before making any git commits.",
+   "input_schema": {
+    "type": "object",
+    "properties": {
      "branch_name": {
       "type": "string",
       "description": "A 2-3 word alphanumeric hyphenated slug for the git branch name"
      }
     },
     "required": [
-     "title",
      "branch_name"
     ]
    }
@@ -426,7 +437,7 @@
  ],
  "system": [
   {
-   "text": "You are an expert coding assistant and architect, with a specialty in Go.\nYou are assisting the user to achieve their goals.\n\nStart by asking concise clarifying questions as needed.\nOnce the intent is clear, work autonomously.\n\nCall the title tool early in the conversation to provide a brief summary of\nwhat the chat is about.\n\nBreak down the overall goal into a series of smaller steps.\n(The first step is often: \"Make a plan.\")\nThen execute each step using tools.\nUpdate the plan if you have encountered problems or learned new information.\n\nWhen in doubt about a step, follow this broad workflow:\n\n- Think about how the current step fits into the overall plan.\n- Do research. Good tool choices: bash, think, keyword_search\n- Make edits.\n- Repeat.\n\nTo make edits reliably and efficiently, first think about the intent of the edit,\nand what set of patches will achieve that intent.\nThen use the patch tool to make those edits. Combine all edits to any given file into a single patch tool call.\n\nFor renames or refactors, consider invoking gopls (via bash).\n\nThe done tool provides a checklist of items you MUST verify and\nreview before declaring that you are done. Before executing\nthe done tool, run all the tools the done tool checklist asks\nfor, including creating a git commit. Do not forget to run tests.\n\n\u003cplatform\u003e\nlinux/amd64\n\u003c/platform\u003e\n\u003cpwd\u003e\n/\n\u003c/pwd\u003e\n\u003cgit_root\u003e\n\n\u003c/git_root\u003e\n\u003cHEAD\u003e\n\n\u003c/HEAD\u003e\n",
+   "text": "You are an expert coding assistant and architect, with a specialty in Go.\nYou are assisting the user to achieve their goals.\n\nStart by asking concise clarifying questions as needed.\nOnce the intent is clear, work autonomously.\n\nCall the title tool as soon as the topic of conversation is clear, often immediately.\n\nBreak down the overall goal into a series of smaller steps.\n(The first step is often: \"Make a plan.\")\nThen execute each step using tools.\nUpdate the plan if you have encountered problems or learned new information.\n\nWhen in doubt about a step, follow this broad workflow:\n\n- Think about how the current step fits into the overall plan.\n- Do research. Good tool choices: bash, think, keyword_search\n- Make edits.\n- Repeat.\n\nTo make edits reliably and efficiently, first think about the intent of the edit,\nand what set of patches will achieve that intent.\nThen use the patch tool to make those edits. Combine all edits to any given file into a single patch tool call.\n\nFor renames or refactors, consider invoking gopls (via bash).\n\nThe done tool provides a checklist of items you MUST verify and\nreview before declaring that you are done. Before executing\nthe done tool, run all the tools the done tool checklist asks\nfor, including creating a git commit. Do not forget to run tests.\n\n\u003cplatform\u003e\nlinux/amd64\n\u003c/platform\u003e\n\u003cpwd\u003e\n/\n\u003c/pwd\u003e\n\u003cgit_root\u003e\n\n\u003c/git_root\u003e\n\u003cHEAD\u003e\n\n\u003c/HEAD\u003e\n",
    "type": "text",
    "cache_control": {
     "type": "ephemeral"
@@ -437,24 +448,24 @@
 Anthropic-Organization-Id: 3c473a21-7208-450a-a9f8-80aebda45c1b

 Anthropic-Ratelimit-Input-Tokens-Limit: 200000

 Anthropic-Ratelimit-Input-Tokens-Remaining: 199000

-Anthropic-Ratelimit-Input-Tokens-Reset: 2025-05-06T02:52:06Z

+Anthropic-Ratelimit-Input-Tokens-Reset: 2025-05-07T13:21:33Z

 Anthropic-Ratelimit-Output-Tokens-Limit: 80000

 Anthropic-Ratelimit-Output-Tokens-Remaining: 80000

-Anthropic-Ratelimit-Output-Tokens-Reset: 2025-05-06T02:52:10Z

+Anthropic-Ratelimit-Output-Tokens-Reset: 2025-05-07T13:21:37Z

 Anthropic-Ratelimit-Requests-Limit: 4000

 Anthropic-Ratelimit-Requests-Remaining: 3999

-Anthropic-Ratelimit-Requests-Reset: 2025-05-06T02:52:05Z

+Anthropic-Ratelimit-Requests-Reset: 2025-05-07T13:21:32Z

 Anthropic-Ratelimit-Tokens-Limit: 280000

 Anthropic-Ratelimit-Tokens-Remaining: 279000

-Anthropic-Ratelimit-Tokens-Reset: 2025-05-06T02:52:06Z

+Anthropic-Ratelimit-Tokens-Reset: 2025-05-07T13:21:33Z

 Cf-Cache-Status: DYNAMIC

-Cf-Ray: 93b52df649282523-SJC

+Cf-Ray: 93c10563284c3c35-SJC

 Content-Type: application/json

-Date: Tue, 06 May 2025 02:52:10 GMT

-Request-Id: req_011CNqX3XPUGXJBNMCWBpMaN

+Date: Wed, 07 May 2025 13:21:37 GMT

+Request-Id: req_011CNtErRMv9jCKtxcqYcYpM

 Server: cloudflare

 Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

 Via: 1.1 google

 X-Robots-Tag: none

 

-{"id":"msg_01De9PhPGpXjkZks75fsrXew","type":"message","role":"assistant","model":"claude-3-7-sonnet-20250219","content":[{"type":"text","text":"I have the following tools available to me:\n\n1. `bash` - Execute shell commands\n2. `keyword_search` - Search for files in a codebase\n3. `think` - Record thoughts and plans\n4. `title` - Set conversation title and create git branch\n5. `done` - Mark task as complete with checklist\n6. `codereview` - Run automated code review\n7. `multiplechoice` - Present multiple choice options to user\n8. `browser_navigate` - Navigate to URL\n9. `browser_click` - Click element using CSS selector\n10. `browser_type` - Type text into input fields\n11. `browser_wait_for` - Wait for element to appear\n12. `browser_get_text` - Get text from element\n13. `browser_eval` - Run JavaScript in browser\n14. `browser_screenshot` - Take screenshot\n15. `browser_scroll_into_view` - Scroll to element\n16. `patch` - Make precise text edits to files\n\nThese tools allow me to help with coding tasks, navigate webpages, make file edits, and manage conversation state."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":4,"cache_creation_input_tokens":3303,"cache_read_input_tokens":0,"output_tokens":259}}
\ No newline at end of file
+{"id":"msg_015VDUUjNoqYLJV9iNxCLub1","type":"message","role":"assistant","model":"claude-3-7-sonnet-20250219","content":[{"type":"text","text":"I'll list the tools available to me:\n\n1. bash - Execute shell commands\n2. keyword_search - Search files for specific terms\n3. think - Record thoughts, notes, or plans\n4. title - Set conversation title\n5. precommit - Create a git branch\n6. done - Mark work as complete with checklist\n7. codereview - Run automated code review\n8. multiplechoice - Present options to the user\n9. browser_navigate - Navigate to a URL\n10. browser_click - Click an element\n11. browser_type - Type into an element\n12. browser_wait_for - Wait for an element\n13. browser_get_text - Get text from an element\n14. browser_eval - Evaluate JavaScript\n15. browser_screenshot - Take a screenshot\n16. browser_scroll_into_view - Scroll to an element\n17. patch - Make precise text edits to files\n\nThese tools allow me to execute commands, search code, navigate browsers, modify files, and help with various development tasks."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":4,"cache_creation_input_tokens":3337,"cache_read_input_tokens":0,"output_tokens":231}}
\ No newline at end of file