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)
+ }
+ })
+ }
+}