From 40b01d989d429356e6697b29d433c2cece9b6235 Mon Sep 17 00:00:00 2001 From: "Kai (Tam Nhu) Tran" <61256810+kaitranntt@users.noreply.github.com> Date: Sun, 22 Mar 2026 23:17:47 -0400 Subject: [PATCH] fix(summoning): prevent identity markdown from corrupting display names (#356) --- internal/http/summoner.go | 3 -- internal/http/summoner_utils.go | 73 ++++++++++++++++++++++++++-- internal/http/summoner_utils_test.go | 59 ++++++++++++++++++++++ 3 files changed, 128 insertions(+), 7 deletions(-) create mode 100644 internal/http/summoner_utils_test.go diff --git a/internal/http/summoner.go b/internal/http/summoner.go index 4e3feeb9..e633900e 100644 --- a/internal/http/summoner.go +++ b/internal/http/summoner.go @@ -38,9 +38,6 @@ var summoningFiles = []string{ // fileTagRe parses content from LLM output. var fileTagRe = regexp.MustCompile(`(?s)\s*(.*?)\s*`) -// identityNameRe extracts the Name field from IDENTITY.md format: - **Name:** value -var identityNameRe = regexp.MustCompile(`(?m)^-\s*\*\*Name:\*\*\s*(.+)$`) - // frontmatterTagRe parses short expertise summary from LLM output. var frontmatterTagRe = regexp.MustCompile(`(?s)\s*(.*?)\s*`) diff --git a/internal/http/summoner_utils.go b/internal/http/summoner_utils.go index 2d2aba59..b435ca58 100644 --- a/internal/http/summoner_utils.go +++ b/internal/http/summoner_utils.go @@ -4,17 +4,82 @@ import ( "strings" ) +var identityFieldPrefixes = []string{ + "- **Name:**", + "- **Creature:**", + "- **Purpose:**", + "- **Vibe:**", + "- **Emoji:**", + "- **Avatar:**", + "Name:", + "Creature:", + "Purpose:", + "Vibe:", + "Emoji:", + "Avatar:", +} + // extractIdentityName extracts the Name field from IDENTITY.md content. -// Matches format: - **Name:** value +// Accepts only an inline Name value and ignores markdown field spillover. func extractIdentityName(content string) string { if content == "" { return "" } - m := identityNameRe.FindStringSubmatch(content) - if len(m) < 2 { + + for _, rawLine := range strings.Split(content, "\n") { + line := strings.TrimSpace(rawLine) + switch { + case strings.HasPrefix(line, "- **Name:**"): + return normalizeIdentityName(strings.TrimSpace(strings.TrimPrefix(line, "- **Name:**"))) + case strings.HasPrefix(line, "Name:"): + return normalizeIdentityName(strings.TrimSpace(strings.TrimPrefix(line, "Name:"))) + } + } + + return "" +} + +func normalizeIdentityName(value string) string { + value = strings.TrimSpace(value) + if value == "" || looksLikeIdentityField(value) { return "" } - return strings.TrimSpace(m[1]) + + for { + next := trimMarkdownWrapper(value) + if next == value { + return value + } + value = strings.TrimSpace(next) + if value == "" || looksLikeIdentityField(value) { + return "" + } + } +} + +func looksLikeIdentityField(value string) bool { + for _, prefix := range identityFieldPrefixes { + if strings.HasPrefix(value, prefix) { + return true + } + } + return false +} + +func trimMarkdownWrapper(value string) string { + wrappers := [][2]string{ + {"**", "**"}, + {"__", "__"}, + {"`", "`"}, + {"*", "*"}, + {"_", "_"}, + } + for _, wrapper := range wrappers { + if strings.HasPrefix(value, wrapper[0]) && strings.HasSuffix(value, wrapper[1]) && len(value) > len(wrapper[0])+len(wrapper[1]) { + return strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(value, wrapper[0]), wrapper[1])) + } + } + return value } // suffixString returns the last n runes of s. diff --git a/internal/http/summoner_utils_test.go b/internal/http/summoner_utils_test.go new file mode 100644 index 00000000..ae31bdc2 --- /dev/null +++ b/internal/http/summoner_utils_test.go @@ -0,0 +1,59 @@ +package http + +import "testing" + +func TestExtractIdentityName(t *testing.T) { + tests := []struct { + name string + content string + want string + }{ + { + name: "extracts inline markdown name", + content: `# IDENTITY.md - Who Am I? + +- **Name:** GoClaw +- **Creature:** AI assistant`, + want: "GoClaw", + }, + { + name: "ignores next bullet when name is blank", + content: `# IDENTITY.md - Who Am I? + +- **Name:** +- **Creature:** AI assistant`, + want: "", + }, + { + name: "rejects inline spillover from another field", + content: `# IDENTITY.md - Who Am I? + +- **Name:** - **Creature:** AI assistant +- **Purpose:** Help users`, + want: "", + }, + { + name: "strips simple markdown wrappers from name", + content: `# IDENTITY.md - Who Am I? + +- **Name:** **GoClaw** +- **Creature:** AI assistant`, + want: "GoClaw", + }, + { + name: "supports plain name format", + content: `# Identity +Name: GoClaw +Emoji: 🤖`, + want: "GoClaw", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := extractIdentityName(tt.content); got != tt.want { + t.Fatalf("extractIdentityName() = %q, want %q", got, tt.want) + } + }) + } +}