From 25d0f40687cfc42d2d5fe5548364454455db6809 Mon Sep 17 00:00:00 2001 From: Andhi Jeannot Date: Tue, 18 Aug 2026 14:51:04 -0500 Subject: [PATCH 1/5] feat: add clean response functionality --- cmd/generate.go | 1 + internal/llm/clean.go | 30 ++++++++++++++++++ internal/llm/clean_test.go | 63 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 internal/llm/clean.go create mode 100644 internal/llm/clean_test.go diff --git a/cmd/generate.go b/cmd/generate.go index f64bd9f..8b20abe 100644 --- a/cmd/generate.go +++ b/cmd/generate.go @@ -110,6 +110,7 @@ func Run(ctx context.Context, opts GenerateOptions) error { if err != nil { return fmt.Errorf("LLM request failed\n → %w", err) } + rawMessage = llm.CleanResponse(rawMessage) if rawMessage == "" { return fmt.Errorf("LLM returned an empty message\n → try again or switch provider") } diff --git a/internal/llm/clean.go b/internal/llm/clean.go new file mode 100644 index 0000000..58dbbaf --- /dev/null +++ b/internal/llm/clean.go @@ -0,0 +1,30 @@ +package llm + +import ( + "regexp" + "strings" +) + +var ( + // fenceRegex matches markdown code fences and captures the content inside. + fenceRegex = regexp.MustCompile("(?s)```(?:[a-z]*\\n)?(.*?)\\n?```") + + // fillerRegex matches common conversational prefixes LLMs use. + fillerRegex = regexp.MustCompile("(?i)^(?:here is (?:the|your)? (?:generated )?commit message|suggested commit message|the commit message is|commit message|generated message):?\\s*") +) + +// CleanResponse removes common LLM conversational filler and markdown fencing +// to extract the actual commit message. +func CleanResponse(input string) string { + output := strings.TrimSpace(input) + + // 1. Remove markdown fences if present. + if matches := fenceRegex.FindStringSubmatch(output); len(matches) > 1 { + output = matches[1] + } + + // 2. Remove common conversational fillers. + output = fillerRegex.ReplaceAllString(output, "") + + return strings.TrimSpace(output) +} diff --git a/internal/llm/clean_test.go b/internal/llm/clean_test.go new file mode 100644 index 0000000..5bc56ab --- /dev/null +++ b/internal/llm/clean_test.go @@ -0,0 +1,63 @@ +package llm + +import ( + "testing" +) + +func TestCleanResponse(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "no cleaning needed", + input: "feat(ui): add new button", + expected: "feat(ui): add new button", + }, + { + name: "markdown fences", + input: "```git\nfeat(ui): add new button\n```", + expected: "feat(ui): add new button", + }, + { + name: "markdown fences with text", + input: "Here is the message:\n```\nfeat(ui): add new button\n```", + expected: "feat(ui): add new button", + }, + { + name: "conversational filler", + input: "Here is your commit message: feat(ui): add new button", + expected: "feat(ui): add new button", + }, + { + name: "case insensitive filler", + input: "COMMIT MESSAGE: feat(ui): add new button", + expected: "feat(ui): add new button", + }, + { + name: "filler with colon and space", + input: "Suggested commit message: feat(ui): add new button ", + expected: "feat(ui): add new button", + }, + { + name: "complex leakage", + input: "Based on the diff, here is the suggested commit message:\n\n```\nfix(core): resolve race condition in buffer\n```\n\nI hope this helps!", + expected: "fix(core): resolve race condition in buffer", + }, + { + name: "empty input", + input: " ", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := CleanResponse(tt.input) + if got != tt.expected { + t.Errorf("CleanResponse(%q) = %q; want %q", tt.input, got, tt.expected) + } + }) + } +} From fe2c060b2baff062eb2454ef3088c09005467a0a Mon Sep 17 00:00:00 2001 From: Andhi Jeannot Date: Tue, 18 Aug 2026 22:08:47 -0500 Subject: [PATCH 2/5] Update build config for trim-llm-response --- .goreleaser.yaml | 8 ++++---- cmd/generate_cobra.go | 18 ++++++++++++++++++ cmd/root.go | 7 ++++++- cmd/root_cobra.go | 1 + cmd/version.go | 21 +++++++++++++++++++++ 5 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 cmd/version.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 2e76ebf..faaa2b1 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -30,10 +30,10 @@ builds: - -trimpath ldflags: - -s -w - - -X main.Version={{.Version}} - - -X main.commit={{.Commit}} - - -X main.date={{.Date}} - - -X main.builtBy=goreleaser + - -X github.com/madstone-tech/git-msg/cmd.Version={{.Version}} + - -X github.com/madstone-tech/git-msg/cmd.Commit={{.Commit}} + - -X github.com/madstone-tech/git-msg/cmd.Date={{.Date}} + - -X github.com/madstone-tech/git-msg/cmd.BuiltBy=goreleaser env: - CGO_ENABLED=0 diff --git a/cmd/generate_cobra.go b/cmd/generate_cobra.go index a1c3903..ec33a73 100644 --- a/cmd/generate_cobra.go +++ b/cmd/generate_cobra.go @@ -46,6 +46,24 @@ func NewGenerateCmd() *cobra.Command { _ = cmd.Flags().MarkHidden("hook-mode") _ = cmd.Flags().MarkHidden("hook-msg-file") _ = cmd.Flags().MarkHidden("hook-source") + _ = cmd.RegisterFlagCompletionFunc("provider", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"openai", "anthropic", "gemini", "ollama"}, cobra.ShellCompDirectiveNoFileComp + }) + _ = cmd.RegisterFlagCompletionFunc("template", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + store, err := prompt.NewFileStore() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + entries, err := store.List() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, e := range entries { + names = append(names, e.Name) + } + return names, cobra.ShellCompDirectiveNoFileComp + }) return cmd } diff --git a/cmd/root.go b/cmd/root.go index 398e354..39d5c4c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,7 +11,12 @@ import ( ) // Version is set at build time via -ldflags "-X main.Version=vX.Y.Z". -var Version = "dev" +var ( + Version = "dev" + Commit = "none" + Date = "unknown" + BuiltBy = "unknown" +) // contextKey is an unexported type for context keys in this package. type contextKey int diff --git a/cmd/root_cobra.go b/cmd/root_cobra.go index a394212..0e2a4fc 100644 --- a/cmd/root_cobra.go +++ b/cmd/root_cobra.go @@ -46,6 +46,7 @@ func newRootCmd(cfgStore config.Store, secrets secret.SecretStore) *cobra.Comman NewConfigCmd(cfgStore), NewPromptCmd(), NewHookCmd(), + NewVersionCmd(), ) return root diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..f6d0194 --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// NewVersionCmd returns a cobra command that prints the binary version and build info. +func NewVersionCmd() *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print the version of git-msg", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("git-msg %s\n", Version) + fmt.Printf("commit: %s\n", Commit) + fmt.Printf("date: %s\n", Date) + fmt.Printf("built by: %s\n", BuiltBy) + }, + } +} From d58e99094640b1a5089e78b9cfbbf908611dea22 Mon Sep 17 00:00:00 2001 From: Andhi Jeannot Date: Tue, 18 Aug 2026 22:38:36 -0500 Subject: [PATCH 3/5] feat(llm): add temperature parameter to LLM responses to control output --- internal/llm/ollama.go | 3 +++ internal/llm/openai.go | 1 + internal/prompt/embedded/conventional.toml | 23 ++++++++++++++++++---- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/internal/llm/ollama.go b/internal/llm/ollama.go index c9526ce..149ec24 100644 --- a/internal/llm/ollama.go +++ b/internal/llm/ollama.go @@ -41,6 +41,9 @@ func (p *OllamaProvider) Generate(ctx context.Context, system, user string) (str } payload := map[string]interface{}{ "model": p.model, + "options": map[string]interface{}{ + "temperature": 0, + }, "messages": []map[string]string{ {"role": "system", "content": system}, {"role": "user", "content": user}, diff --git a/internal/llm/openai.go b/internal/llm/openai.go index aac3c73..56e4e45 100644 --- a/internal/llm/openai.go +++ b/internal/llm/openai.go @@ -41,6 +41,7 @@ func NewOpenAIProviderWithEndpoint(model, apiKey, endpoint string) *OpenAIProvid func (p *OpenAIProvider) Generate(ctx context.Context, system, user string) (string, error) { payload := map[string]interface{}{ "model": p.model, + "temperature": 0, "messages": []map[string]string{ {"role": "system", "content": system}, {"role": "user", "content": user}, diff --git a/internal/prompt/embedded/conventional.toml b/internal/prompt/embedded/conventional.toml index bb84f28..dfc57a1 100644 --- a/internal/prompt/embedded/conventional.toml +++ b/internal/prompt/embedded/conventional.toml @@ -2,10 +2,25 @@ name = "conventional" description = "Conventional Commits with branch and log context" system = """ -You are a git commit message generator. -Follow the Conventional Commits specification (type(scope): subject). -Keep the subject line under 72 characters. -Output only the commit message. No explanation, no markdown fencing. +You are an expert git commit message generator. + +CORE DIRECTIVE: +You MUST base your message ONLY on the provided staged diff. +1. ANALYZE: First, identify exactly which files changed and what the literal changes are. +2. GROUND: If the diff is small (e.g., a few lines in a config or prompt file), describe the literal change (e.g., "update prompt text") rather than inventing a high-level feature. +3. VERIFY: Before outputting, ensure every claim in your message is supported by a line in the diff. + +NEVER: +- Invent files, dependencies, or features not present in the diff. +- Refer to changes as "improvements" or "updates" without specific evidence. +- Hallucinate a "standard" commit message based on the project type. + +Follow the Conventional Commits specification: +1. FORMAT: ( + - Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert. + - Subject: imperative mood, no period, < 72 chars. +2. DETAIL: Provide a body for significant changes explaining the "what" and "why". +3. OUTPUT: Output ONLY the final commit message. No conversational filler, no markdown fences. """ user = """ From c63027db4ed1c2269f33cc01f895187a0bfccd9d Mon Sep 17 00:00:00 2001 From: Andhi Jeannot Date: Tue, 18 Aug 2026 22:47:38 -0500 Subject: [PATCH 4/5] chore: fix linting issues (gofmt and staticcheck) --- cmd/root.go | 8 ++++---- internal/llm/clean.go | 2 +- internal/llm/openai.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 39d5c4c..374a246 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -12,10 +12,10 @@ import ( // Version is set at build time via -ldflags "-X main.Version=vX.Y.Z". var ( - Version = "dev" - Commit = "none" - Date = "unknown" - BuiltBy = "unknown" + Version = "dev" + Commit = "none" + Date = "unknown" + BuiltBy = "unknown" ) // contextKey is an unexported type for context keys in this package. diff --git a/internal/llm/clean.go b/internal/llm/clean.go index 58dbbaf..9a5d955 100644 --- a/internal/llm/clean.go +++ b/internal/llm/clean.go @@ -10,7 +10,7 @@ var ( fenceRegex = regexp.MustCompile("(?s)```(?:[a-z]*\\n)?(.*?)\\n?```") // fillerRegex matches common conversational prefixes LLMs use. - fillerRegex = regexp.MustCompile("(?i)^(?:here is (?:the|your)? (?:generated )?commit message|suggested commit message|the commit message is|commit message|generated message):?\\s*") + fillerRegex = regexp.MustCompile(`(?i)^(?:here is (?:the|your)? (?:generated )?commit message|suggested commit message|the commit message is|commit message|generated message):?\s*`) ) // CleanResponse removes common LLM conversational filler and markdown fencing diff --git a/internal/llm/openai.go b/internal/llm/openai.go index 56e4e45..d9368e2 100644 --- a/internal/llm/openai.go +++ b/internal/llm/openai.go @@ -40,7 +40,7 @@ func NewOpenAIProviderWithEndpoint(model, apiKey, endpoint string) *OpenAIProvid func (p *OpenAIProvider) Generate(ctx context.Context, system, user string) (string, error) { payload := map[string]interface{}{ - "model": p.model, + "model": p.model, "temperature": 0, "messages": []map[string]string{ {"role": "system", "content": system}, From 6b2bde3eb1d0a6f0d0988473a9239cba28852897 Mon Sep 17 00:00:00 2001 From: Andhi Jeannot Date: Tue, 18 Aug 2026 22:48:19 -0500 Subject: [PATCH 5/5] chore: rename cmd/version.go to cmd/version_cobra.go to satisfy Constitution Principle II --- cmd/{version.go => version_cobra.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename cmd/{version.go => version_cobra.go} (100%) diff --git a/cmd/version.go b/cmd/version_cobra.go similarity index 100% rename from cmd/version.go rename to cmd/version_cobra.go