claudetool: change title tool to precommit, add commit style guidance
diff --git a/claudetool/pre-commit.go b/claudetool/pre-commit.go
new file mode 100644
index 0000000..7282948
--- /dev/null
+++ b/claudetool/pre-commit.go
@@ -0,0 +1,93 @@
+package claudetool
+
+import (
+	"context"
+	"fmt"
+	"os/exec"
+	"strings"
+
+	"sketch.dev/llm/conversation"
+)
+
+// CommitMessageStyleHint explains how to find commit messages representative of this repository's style.
+func CommitMessageStyleHint(ctx context.Context, repoRoot string) (string, error) {
+	commitSHAs, err := representativeCommitSHAs(ctx, repoRoot)
+	if err != nil {
+		return "", err
+	}
+
+	buf := new(strings.Builder)
+	if len(commitSHAs) > 0 {
+		fmt.Fprint(buf, "To see representative commit messages for this repository, run:\n\n")
+		fmt.Fprintf(buf, "git show -s --format='<commit_message>%%n%%B</commit_message>' %s\n\n", strings.Join(commitSHAs, " "))
+		fmt.Fprint(buf, "Please run this EXACT command and follow their style when writing commit messages.\n")
+	}
+
+	return buf.String(), nil
+}
+
+// representativeCommitSHAs analyze recent commits and selects some representative ones.
+func representativeCommitSHAs(ctx context.Context, repoRoot string) ([]string, error) {
+	cmd := exec.Command("git", "log", "-n", "25", `--format=<commit_message hash="%H">%n%B</commit_message>`)
+	cmd.Dir = repoRoot
+	out, err := cmd.CombinedOutput()
+	if err != nil {
+		return nil, fmt.Errorf("git log failed: %w\n%s", err, out)
+	}
+	commits := strings.TrimSpace(string(out))
+	if commits == "" {
+		return nil, fmt.Errorf("no commits found in repository")
+	}
+
+	info := conversation.ToolCallInfoFromContext(ctx)
+	sub := info.Convo.SubConvo()
+
+	sub.SystemPrompt = `You are an expert Git commit analyzer.
+
+Your task is to analyze the provided commit messages and select the most representative examples that demonstrate this repository's commit style.
+
+Identify consistent patterns in:
+- Formatting conventions
+- Language and tone
+- Structure and organization
+- Special notations or tags
+
+Select up to 3 commit hashes that best exemplify the repository's commit style.
+
+Provide ONLY the commit hashes, one per line. No additional text, formatting, or commentary.
+`
+
+	resp, err := sub.SendUserTextMessage(commits)
+	if err != nil {
+		return nil, fmt.Errorf("error from Claude: %w", err)
+	}
+
+	if len(resp.Content) != 1 {
+		return nil, fmt.Errorf("unexpected response: %v", resp)
+	}
+	response := resp.Content[0].Text
+
+	var result []string
+	for line := range strings.Lines(response) {
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+		if isHexString(line) && (len(line) >= 7 && len(line) <= 40) {
+			result = append(result, line)
+		}
+	}
+
+	result = result[:min(len(result), 3)]
+	return result, nil
+}
+
+// isHexString reports whether a string only contains hexadecimal characters
+func isHexString(s string) bool {
+	for _, c := range s {
+		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
+			return false
+		}
+	}
+	return true
+}