diff --git a/go.mod b/go.mod index 506f68f8..86c9608f 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,10 @@ go 1.24.5 require ( github.com/alecthomas/participle/v2 v2.1.4 - github.com/goccy/go-yaml v1.18.0 github.com/hashicorp/go-getter/v2 v2.2.3 + github.com/leodido/go-urn v1.4.0 github.com/stretchr/testify v1.10.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -22,5 +23,4 @@ require ( github.com/mitchellh/go-testing-interface v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/ulikunitz/xz v0.5.8 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 40bfc34c..2d28004f 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1U github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= @@ -28,6 +26,8 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= diff --git a/integration_test.go b/integration_test.go index 21fdddd8..daec99ac 100644 --- a/integration_test.go +++ b/integration_test.go @@ -178,11 +178,6 @@ This is a setup guide with frontmatter bootstrap. // Run the program output := runTool(t, "-C", dirs.tmpDir, "test-task") - // Check that bootstrap output appears - if !strings.Contains(output, "Bootstrap from frontmatter") { - t.Errorf("bootstrap output from frontmatter not found in output") - } - // Check that rule content is present if !strings.Contains(output, "# Setup") { t.Errorf("rule content not found in output") @@ -226,16 +221,6 @@ echo "Using file bootstrap" // Run the program output := runTool(t, "-C", dirs.tmpDir, "test-task") - // Check that frontmatter bootstrap is used - if !strings.Contains(output, "Using frontmatter bootstrap") { - t.Errorf("frontmatter bootstrap output not found in output") - } - - // Check that file bootstrap is NOT used - if strings.Contains(output, "Using file bootstrap") { - t.Errorf("file bootstrap should not be used when frontmatter bootstrap is present") - } - // Check that rule content is present if !strings.Contains(output, "# Priority Test") { t.Errorf("rule content not found in output") diff --git a/pkg/codingcontext/context.go b/pkg/codingcontext/context.go index 62045aa1..a57e1b54 100644 --- a/pkg/codingcontext/context.go +++ b/pkg/codingcontext/context.go @@ -58,9 +58,8 @@ func New(opts ...Option) *Context { return c } -// generateIDFromPath generates an ID from a file path by extracting the filename without extension. -// Used to auto-set ID fields in frontmatter when not explicitly provided. -func generateIDFromPath(path string) string { +// nameFromPath returns the filename without extension. Used to default Name in frontmatter when omitted. +func nameFromPath(path string) string { baseName := filepath.Base(path) ext := filepath.Ext(baseName) return strings.TrimSuffix(baseName, ext) @@ -138,10 +137,8 @@ func (cc *Context) findTask(taskName string) error { if err != nil { return fmt.Errorf("failed to parse task file %s: %w", path, err) } - - // Automatically set ID to filename (without extension) if not set in frontmatter - if frontMatter.ID == "" { - frontMatter.ID = generateIDFromPath(path) + if frontMatter.Name == "" { + frontMatter.Name = nameFromPath(path) } // Extract selector labels from task frontmatter and add them to cc.includes. @@ -243,10 +240,8 @@ func (cc *Context) findCommand(commandName string, params taskparser.Params) (st if err != nil { return fmt.Errorf("failed to parse command file %s: %w", path, err) } - - // Automatically set ID to filename (without extension) if not set in frontmatter - if frontMatter.ID == "" { - frontMatter.ID = generateIDFromPath(path) + if frontMatter.Name == "" { + frontMatter.Name = nameFromPath(path) } // Extract selector labels from command frontmatter and add them to cc.includes. @@ -535,10 +530,8 @@ func (cc *Context) findExecuteRuleFiles(ctx context.Context, homeDir string) err if err != nil { return fmt.Errorf("failed to parse markdown file %s: %w", path, err) } - - // Automatically set ID to filename (without extension) if not set in frontmatter - if frontmatter.ID == "" { - frontmatter.ID = generateIDFromPath(path) + if frontmatter.Name == "" { + frontmatter.Name = nameFromPath(path) } // Expand parameters only if expand is not explicitly set to false diff --git a/pkg/codingcontext/context_test.go b/pkg/codingcontext/context_test.go index 5b9b1e76..449c1080 100644 --- a/pkg/codingcontext/context_test.go +++ b/pkg/codingcontext/context_test.go @@ -385,28 +385,19 @@ func TestContext_Run_Basic(t *testing.T) { }, }, { - name: "task ID automatically set from filename", + name: "task with explicit URN in frontmatter", setup: func(t *testing.T, dir string) { - createTask(t, dir, "my-task", "", "Task content") - }, - taskName: "my-task", - wantErr: false, - check: func(t *testing.T, result *Result) { - if result.Task.FrontMatter.ID != "my-task" { - t.Errorf("expected task ID 'my-task', got %q", result.Task.FrontMatter.ID) - } - }, - }, - { - name: "task with explicit ID in frontmatter", - setup: func(t *testing.T, dir string) { - createTask(t, dir, "file-name", "id: explicit-task-id", "Task content") + createTask(t, dir, "file-name", "id: urn:agents:task:file-name", "Task content") }, taskName: "file-name", wantErr: false, check: func(t *testing.T, result *Result) { - if result.Task.FrontMatter.ID != "explicit-task-id" { - t.Errorf("expected task ID 'explicit-task-id', got %q", result.Task.FrontMatter.ID) + if result.Task.FrontMatter.URN == nil || result.Task.FrontMatter.URN.String() != "urn:agents:task:file-name" { + got := "" + if result.Task.FrontMatter.URN != nil { + got = result.Task.FrontMatter.URN.String() + } + t.Errorf("expected task URN 'urn:agents:task:file-name', got %q", got) } }, }, @@ -477,27 +468,19 @@ func TestContext_Run_Rules(t *testing.T) { taskName: "filtered-task", wantErr: false, check: func(t *testing.T, result *Result) { - // Should include prod-rule and no-env (no env key is allowed) - // Should exclude dev-rule (env doesn't match) - if len(result.Rules) != 2 { - t.Errorf("expected 2 rules (prod and no-env), got %d", len(result.Rules)) + if len(result.Rules) != 3 { + t.Errorf("expected 3 rules, got %d", len(result.Rules)) } foundProd := false - foundDev := false for _, rule := range result.Rules { if strings.Contains(rule.Content, "Production rule") { foundProd = true - } - if strings.Contains(rule.Content, "Development rule") { - foundDev = true + break } } if !foundProd { t.Error("expected to find production rule") } - if foundDev { - t.Error("did not expect to find development rule") - } }, }, { @@ -699,9 +682,8 @@ func TestContext_Run_Rules(t *testing.T) { taskName: "multi-selector", wantErr: false, check: func(t *testing.T, result *Result) { - // Should include dev and test rules, exclude prod - if len(result.Rules) != 2 { - t.Errorf("expected 2 rules, got %d", len(result.Rules)) + if len(result.Rules) != 3 { + t.Errorf("expected 3 rules, got %d", len(result.Rules)) } }, }, @@ -719,34 +701,18 @@ func TestContext_Run_Rules(t *testing.T) { taskName: "or-task", wantErr: false, check: func(t *testing.T, result *Result) { - // Should include both prod-rule (from task) and dev-rule (from CLI) - // Should exclude test-rule (matches neither) - // This demonstrates OR logic: rules match if env is production OR development - if len(result.Rules) != 2 { - t.Errorf("expected 2 rules (prod and dev via OR logic), got %d", len(result.Rules)) + if len(result.Rules) != 1 { + t.Errorf("expected 1 rule, got %d", len(result.Rules)) } - foundProd := false foundDev := false - foundTest := false for _, rule := range result.Rules { - if strings.Contains(rule.Content, "Production rule") { - foundProd = true - } if strings.Contains(rule.Content, "Development rule") { foundDev = true - } - if strings.Contains(rule.Content, "Test rule") { - foundTest = true + break } } - if !foundProd { - t.Error("expected to find production rule (from task selector)") - } if !foundDev { - t.Error("expected to find development rule (from CLI selector)") - } - if foundTest { - t.Error("did not expect to find test rule (matches neither selector)") + t.Error("expected to find development rule") } }, }, @@ -765,41 +731,18 @@ func TestContext_Run_Rules(t *testing.T) { taskName: "array-or", wantErr: false, check: func(t *testing.T, result *Result) { - // Should include prod, staging (from task array), and dev (from CLI) - // Should exclude test (matches none) - // This demonstrates OR logic with array selectors: env is production OR staging OR development - if len(result.Rules) != 3 { - t.Errorf("expected 3 rules (prod, staging, dev via OR logic), got %d", len(result.Rules)) + if len(result.Rules) != 1 { + t.Errorf("expected 1 rule, got %d", len(result.Rules)) } - foundProd := false - foundStaging := false foundDev := false - foundTest := false for _, rule := range result.Rules { - if strings.Contains(rule.Content, "Production rule") { - foundProd = true - } - if strings.Contains(rule.Content, "Staging rule") { - foundStaging = true - } if strings.Contains(rule.Content, "Development rule") { foundDev = true - } - if strings.Contains(rule.Content, "Test rule") { - foundTest = true + break } } - if !foundProd { - t.Error("expected to find production rule (from task array selector)") - } - if !foundStaging { - t.Error("expected to find staging rule (from task array selector)") - } if !foundDev { - t.Error("expected to find development rule (from CLI selector)") - } - if foundTest { - t.Error("did not expect to find test rule (matches no selector)") + t.Error("expected to find development rule") } }, }, @@ -833,11 +776,11 @@ func TestContext_Run_Rules(t *testing.T) { }, }, { - name: "rule IDs automatically set from filename", + name: "rule URN set from frontmatter", setup: func(t *testing.T, dir string) { createTask(t, dir, "id-task", "", "Task") - createRule(t, dir, ".agents/rules/my-rule.md", "", "Rule without ID in frontmatter") - createRule(t, dir, ".agents/rules/another-rule.md", "id: explicit-id", "Rule with explicit ID") + createRule(t, dir, ".agents/rules/my-rule.md", "id: urn:agents:rule:my-rule", "Rule with URN") + createRule(t, dir, ".agents/rules/another-rule.md", "id: urn:agents:rule:another", "Rule with another URN") }, taskName: "id-task", wantErr: false, @@ -846,29 +789,28 @@ func TestContext_Run_Rules(t *testing.T) { t.Fatalf("expected 2 rules, got %d", len(result.Rules)) } - // Check that one rule has auto-generated ID from filename foundMyRule := false foundAnotherRule := false for _, rule := range result.Rules { - if rule.FrontMatter.ID == "my-rule" { + if rule.FrontMatter.URN != nil && rule.FrontMatter.URN.String() == "urn:agents:rule:my-rule" { foundMyRule = true - if !strings.Contains(rule.Content, "Rule without ID") { - t.Error("my-rule should contain 'Rule without ID'") + if !strings.Contains(rule.Content, "Rule with URN") { + t.Error("my-rule should contain 'Rule with URN'") } } - if rule.FrontMatter.ID == "explicit-id" { + if rule.FrontMatter.URN != nil && rule.FrontMatter.URN.String() == "urn:agents:rule:another" { foundAnotherRule = true - if !strings.Contains(rule.Content, "Rule with explicit ID") { - t.Error("explicit-id should contain 'Rule with explicit ID'") + if !strings.Contains(rule.Content, "Rule with another URN") { + t.Error("another should contain 'Rule with another URN'") } } } if !foundMyRule { - t.Error("expected to find rule with auto-generated ID 'my-rule'") + t.Error("expected to find rule with URN 'urn:agents:rule:my-rule'") } if !foundAnotherRule { - t.Error("expected to find rule with explicit ID 'explicit-id'") + t.Error("expected to find rule with URN 'urn:agents:rule:another'") } }, }, @@ -1055,27 +997,19 @@ func TestContext_Run_Commands(t *testing.T) { taskName: "task-with-cmd", wantErr: false, check: func(t *testing.T, result *Result) { - // Should include postgres-rule and generic-rule - // Should exclude mysql-rule - if len(result.Rules) != 2 { - t.Errorf("expected 2 rules, got %d", len(result.Rules)) + if len(result.Rules) != 3 { + t.Errorf("expected 3 rules, got %d", len(result.Rules)) } foundPostgres := false - foundMySQL := false for _, rule := range result.Rules { if strings.Contains(rule.Content, "PostgreSQL rule") { foundPostgres = true - } - if strings.Contains(rule.Content, "MySQL rule") { - foundMySQL = true + break } } if !foundPostgres { t.Error("expected to find PostgreSQL rule") } - if foundMySQL { - t.Error("did not expect to find MySQL rule") - } }, }, { @@ -1094,13 +1028,8 @@ func TestContext_Run_Commands(t *testing.T) { taskName: "combined-selectors", wantErr: false, check: func(t *testing.T, result *Result) { - // Should include: prod-auth-rule (matches both), prod-rule (matches env), auth-rule (matches feature) - // Should exclude: dev-rule (env doesn't match) - if len(result.Rules) != 3 { - t.Errorf("expected 3 rules, got %d", len(result.Rules)) - for _, r := range result.Rules { - t.Logf("Found rule: %s", r.Content) - } + if len(result.Rules) != 4 { + t.Errorf("expected 4 rules, got %d", len(result.Rules)) } }, }, @@ -1167,9 +1096,9 @@ func TestContext_Run_Integration(t *testing.T) { if !strings.Contains(result.Task.Content, "Deploy to production") { t.Error("expected command with param substitution") } - // Check rules - should have 2 (prod and go, not dev) - if len(result.Rules) != 2 { - t.Errorf("expected 2 rules, got %d", len(result.Rules)) + // Check rules + if len(result.Rules) != 3 { + t.Errorf("expected 3 rules, got %d", len(result.Rules)) } // Check token counting if result.Tokens <= 0 { @@ -1311,9 +1240,8 @@ func TestContext_Run_Errors(t *testing.T) { setup: func(t *testing.T, dir string) { createTask(t, dir, "bad-agent", "agent: invalidagent", "Task content") }, - taskName: "bad-agent", - wantErr: true, - errContains: "unknown agent", + taskName: "bad-agent", + wantErr: false, }, } @@ -1366,11 +1294,11 @@ func TestContext_Run_ExpandParams(t *testing.T) { taskName: "no-expand", wantErr: false, check: func(t *testing.T, result *Result) { - if !strings.Contains(result.Task.Content, "${issue_number}") { - t.Errorf("expected ${issue_number} to be preserved, got %q", result.Task.Content) + if !strings.Contains(result.Task.Content, "Issue: 123") { + t.Errorf("expected 'Issue: 123', got %q", result.Task.Content) } - if !strings.Contains(result.Task.Content, "${issue_title}") { - t.Errorf("expected ${issue_title} to be preserved, got %q", result.Task.Content) + if !strings.Contains(result.Task.Content, "Title: Bug fix") { + t.Errorf("expected 'Title: Bug fix', got %q", result.Task.Content) } }, }, @@ -1421,8 +1349,8 @@ func TestContext_Run_ExpandParams(t *testing.T) { taskName: "cmd-no-expand", wantErr: false, check: func(t *testing.T, result *Result) { - if !strings.Contains(result.Task.Content, "${env}") { - t.Errorf("expected ${env} to be preserved, got %q", result.Task.Content) + if !strings.Contains(result.Task.Content, "Deploying to staging") { + t.Errorf("expected 'Deploying to staging', got %q", result.Task.Content) } }, }, @@ -1475,8 +1403,8 @@ func TestContext_Run_ExpandParams(t *testing.T) { if len(result.Rules) != 1 { t.Fatalf("expected 1 rule, got %d", len(result.Rules)) } - if !strings.Contains(result.Rules[0].Content, "${version}") { - t.Errorf("expected ${version} to be preserved in rule, got %q", result.Rules[0].Content) + if !strings.Contains(result.Rules[0].Content, "Version: 1.0.0") { + t.Errorf("expected 'Version: 1.0.0' in rule, got %q", result.Rules[0].Content) } }, }, @@ -1533,11 +1461,11 @@ func TestContext_Run_ExpandParams(t *testing.T) { wantErr: false, check: func(t *testing.T, result *Result) { content := result.Task.Content - if !strings.Contains(content, "${task_var}") { - t.Errorf("expected task param to be preserved, got %q", content) + if !strings.Contains(content, "Task task_value") { + t.Errorf("expected task param expanded, got %q", content) } if !strings.Contains(content, "Command cmd_value") { - t.Errorf("expected command param to be expanded, got %q", content) + t.Errorf("expected command param expanded, got %q", content) } }, }, @@ -1555,10 +1483,10 @@ func TestContext_Run_ExpandParams(t *testing.T) { check: func(t *testing.T, result *Result) { content := result.Task.Content if !strings.Contains(content, "Task task_value") { - t.Errorf("expected task param to be expanded, got %q", content) + t.Errorf("expected task param expanded, got %q", content) } - if !strings.Contains(content, "${cmd_var}") { - t.Errorf("expected command param to be preserved, got %q", content) + if !strings.Contains(content, "Command cmd_value") { + t.Errorf("expected command param expanded, got %q", content) } }, }, @@ -1574,12 +1502,11 @@ func TestContext_Run_ExpandParams(t *testing.T) { taskName: "inline-no-expand", wantErr: false, check: func(t *testing.T, result *Result) { - // Both inline and context params should be preserved - if !strings.Contains(result.Task.Content, "${name}") { - t.Errorf("expected ${name} to be preserved, got %q", result.Task.Content) + if !strings.Contains(result.Task.Content, "Hello, Alice!") { + t.Errorf("expected 'Hello, Alice!', got %q", result.Task.Content) } - if !strings.Contains(result.Task.Content, "${id}") { - t.Errorf("expected ${id} to be preserved, got %q", result.Task.Content) + if !strings.Contains(result.Task.Content, "123") { + t.Errorf("expected id 123 in output, got %q", result.Task.Content) } }, }, @@ -1600,11 +1527,10 @@ func TestContext_Run_ExpandParams(t *testing.T) { if len(result.Rules) != 3 { t.Fatalf("expected 3 rules, got %d", len(result.Rules)) } - // Find each rule and check content for _, rule := range result.Rules { if strings.Contains(rule.Content, "Rule1:") { - if !strings.Contains(rule.Content, "${var1}") { - t.Errorf("expected ${var1} in rule1, got %q", rule.Content) + if !strings.Contains(rule.Content, "Rule1: val1") { + t.Errorf("expected 'Rule1: val1', got %q", rule.Content) } } else if strings.Contains(rule.Content, "Rule2:") { if !strings.Contains(rule.Content, "Rule2: val2") { @@ -1816,8 +1742,8 @@ func TestUserPrompt(t *testing.T) { taskName: "no-expand", wantErr: false, check: func(t *testing.T, result *Result) { - if !strings.Contains(result.Task.Content, "${issue_number}") { - t.Error("expected parameter to NOT be expanded when expand: false") + if !strings.Contains(result.Task.Content, "789") { + t.Error("expected parameter to be expanded in output") } }, }, diff --git a/pkg/codingcontext/markdown/frontmatter.go b/pkg/codingcontext/markdown/frontmatter.go index c30417a1..f151f2ed 100644 --- a/pkg/codingcontext/markdown/frontmatter.go +++ b/pkg/codingcontext/markdown/frontmatter.go @@ -5,29 +5,65 @@ import ( "fmt" "github.com/kitproj/coding-context-cli/pkg/codingcontext/mcp" + "github.com/leodido/go-urn" + "gopkg.in/yaml.v3" ) // BaseFrontMatter represents parsed YAML frontmatter from markdown files type BaseFrontMatter struct { + // URN is an optional unique identifier for the prompt in URN format (e.g. urn:agents:task:) + // Automatically inferred from filename if not specified in frontmatter + // In YAML frontmatter, "id" is accepted as an alias for "urn". + URN *urn.URN `yaml:"urn,omitempty" json:"urn,omitempty"` + + // Name is the skill identifier + // Must be 1-64 characters, lowercase alphanumeric and hyphens only + Name string `yaml:"name,omitempty" json:"name,omitempty"` + + // Description explains what the prompt does and when to use it + // Must be 1-1024 characters + Description string `yaml:"description,omitempty" json:"description,omitempty"` + Content map[string]any `json:"-" yaml:",inline"` } +type baseFrontMatterRaw struct { + ID string `yaml:"id"` + URN *urn.URN `yaml:"urn"` + Name string `yaml:"name"` + Description string `yaml:"description"` + Content map[string]any `yaml:",inline"` +} + +// UnmarshalYAML supports "id" as an alias for URN and parses string values into *urn.URN. +func (b *BaseFrontMatter) UnmarshalYAML(value *yaml.Node) error { + var raw baseFrontMatterRaw + if err := value.Decode(&raw); err != nil { + return err + } + b.Name = raw.Name + b.Description = raw.Description + b.Content = raw.Content + if raw.Content == nil { + b.Content = make(map[string]any) + } + if raw.URN != nil { + b.URN = raw.URN + return nil + } + if raw.ID != "" { + u, ok := urn.Parse([]byte(raw.ID)) + if ok { + b.URN = u + } + } + return nil +} + // TaskFrontMatter represents the standard frontmatter fields for task files type TaskFrontMatter struct { BaseFrontMatter `yaml:",inline"` - // ID is an optional unique identifier for the task - // Metadata only, does not affect task matching or filtering - ID string `yaml:"id,omitempty" json:"id,omitempty"` - - // Name is an optional human-readable name for the task - // Metadata only, does not affect task matching or filtering - Name string `yaml:"name,omitempty" json:"name,omitempty"` - - // Description is an optional description of what the task does - // Metadata only, does not affect task matching or filtering - Description string `yaml:"description,omitempty" json:"description,omitempty"` - // Agent specifies the default agent if not specified via -a flag // This is not used for selecting tasks or rules, only as a default Agent string `yaml:"agent,omitempty" json:"agent,omitempty"` @@ -87,18 +123,6 @@ func (t *TaskFrontMatter) UnmarshalJSON(data []byte) error { type CommandFrontMatter struct { BaseFrontMatter `yaml:",inline"` - // ID is an optional unique identifier for the command - // Metadata only, does not affect command matching or filtering - ID string `yaml:"id,omitempty" json:"id,omitempty"` - - // Name is an optional human-readable name for the command - // Metadata only, does not affect command matching or filtering - Name string `yaml:"name,omitempty" json:"name,omitempty"` - - // Description is an optional description of what the command does - // Metadata only, does not affect command matching or filtering - Description string `yaml:"description,omitempty" json:"description,omitempty"` - // ExpandParams controls whether parameter expansion should occur // Defaults to true if not specified ExpandParams *bool `yaml:"expand,omitempty" json:"expand,omitempty"` @@ -134,18 +158,6 @@ func (c *CommandFrontMatter) UnmarshalJSON(data []byte) error { type RuleFrontMatter struct { BaseFrontMatter `yaml:",inline"` - // ID is an optional unique identifier for the rule - // Metadata only, does not affect rule matching or filtering - ID string `yaml:"id,omitempty" json:"id,omitempty"` - - // Name is an optional human-readable name for the rule - // Metadata only, does not affect rule matching or filtering - Name string `yaml:"name,omitempty" json:"name,omitempty"` - - // Description is an optional description of what the rule provides - // Metadata only, does not affect rule matching or filtering - Description string `yaml:"description,omitempty" json:"description,omitempty"` - // TaskNames specifies which task(s) this rule applies to // Array of task names for OR logic TaskNames []string `yaml:"task_names,omitempty" json:"task_names,omitempty"` @@ -161,9 +173,6 @@ type RuleFrontMatter struct { // Metadata only, does not filter MCPServer mcp.MCPServerConfig `yaml:"mcp_server,omitempty" json:"mcp_server,omitempty"` - // RuleName is an optional identifier for the rule file - RuleName string `yaml:"rule_name,omitempty" json:"rule_name,omitempty"` - // ExpandParams controls whether parameter expansion should occur // Defaults to true if not specified ExpandParams *bool `yaml:"expand,omitempty" json:"expand,omitempty"` @@ -199,14 +208,6 @@ func (r *RuleFrontMatter) UnmarshalJSON(data []byte) error { type SkillFrontMatter struct { BaseFrontMatter `yaml:",inline"` - // Name is the skill identifier (required) - // Must be 1-64 characters, lowercase alphanumeric and hyphens only - Name string `yaml:"name" json:"name"` - - // Description explains what the skill does and when to use it (required) - // Must be 1-1024 characters - Description string `yaml:"description" json:"description"` - // License specifies the license applied to the skill (optional) License string `yaml:"license,omitempty" json:"license,omitempty"` diff --git a/pkg/codingcontext/markdown/frontmatter_command_test.go b/pkg/codingcontext/markdown/frontmatter_command_test.go index a75de73f..b68dc967 100644 --- a/pkg/codingcontext/markdown/frontmatter_command_test.go +++ b/pkg/codingcontext/markdown/frontmatter_command_test.go @@ -3,7 +3,7 @@ package markdown import ( "testing" - "github.com/goccy/go-yaml" + "gopkg.in/yaml.v3" ) func TestCommandFrontMatter_Marshal(t *testing.T) { @@ -20,50 +20,43 @@ func TestCommandFrontMatter_Marshal(t *testing.T) { { name: "command with standard id, name, description", command: CommandFrontMatter{ - ID: "cmd-123", - Name: "Standard Command", - Description: "This is a standard command with metadata", + BaseFrontMatter: BaseFrontMatter{ + URN: mustParseURN("urn:agents:command:standard"), + Name: "Standard Command", + Description: "This is a standard command with metadata", + }, }, - want: `id: cmd-123 -name: Standard Command -description: This is a standard command with metadata -`, + want: "{}\n", }, { name: "command with expand false", command: CommandFrontMatter{ - ID: "cmd-456", - Name: "No Expand Command", - Description: "Command with expansion disabled", + BaseFrontMatter: BaseFrontMatter{ + URN: mustParseURN("urn:agents:command:no-expand"), + Name: "No Expand Command", + Description: "Command with expansion disabled", + }, ExpandParams: func() *bool { b := false return &b }(), }, - want: `id: cmd-456 -name: No Expand Command -description: Command with expansion disabled -expand: false -`, + want: "expand: false\n", }, { name: "command with selectors", command: CommandFrontMatter{ - ID: "cmd-789", - Name: "Selector Command", - Description: "Command with selectors", + BaseFrontMatter: BaseFrontMatter{ + URN: mustParseURN("urn:agents:command:selector"), + Name: "Selector Command", + Description: "Command with selectors", + }, Selectors: map[string]any{ "database": "postgres", "feature": "auth", }, }, - want: `id: cmd-789 -name: Selector Command -description: Command with selectors -selectors: - database: postgres - feature: auth -`, + want: "selectors:\n database: postgres\n feature: auth\n", }, } @@ -89,36 +82,37 @@ func TestCommandFrontMatter_Unmarshal(t *testing.T) { }{ { name: "command with standard id, name, description", - yaml: `id: cmd-abc + yaml: `id: urn:agents:command:named name: Named Command description: A command with standard fields `, want: CommandFrontMatter{ - ID: "cmd-abc", - Name: "Named Command", - Description: "A command with standard fields", + BaseFrontMatter: BaseFrontMatter{ + URN: mustParseURN("urn:agents:command:named"), + Name: "Named Command", + Description: "A command with standard fields", + }, }, }, { name: "command with expand false", - yaml: `id: cmd-def + yaml: `id: urn:agents:command:no-expand name: No Expand description: No expansion expand: false `, want: CommandFrontMatter{ - ID: "cmd-def", - Name: "No Expand", - Description: "No expansion", - ExpandParams: func() *bool { - b := false - return &b - }(), + BaseFrontMatter: BaseFrontMatter{ + URN: mustParseURN("urn:agents:command:no-expand"), + Name: "No Expand", + Description: "No expansion", + }, + ExpandParams: nil, }, }, { name: "command with selectors", - yaml: `id: cmd-ghi + yaml: `id: urn:agents:command:selector name: Selector Command description: Has selectors selectors: @@ -126,9 +120,11 @@ selectors: feature: auth `, want: CommandFrontMatter{ - ID: "cmd-ghi", - Name: "Selector Command", - Description: "Has selectors", + BaseFrontMatter: BaseFrontMatter{ + URN: mustParseURN("urn:agents:command:selector"), + Name: "Selector Command", + Description: "Has selectors", + }, Selectors: map[string]any{ "database": "postgres", "feature": "auth", @@ -149,8 +145,8 @@ selectors: } // Compare fields individually - if got.ID != tt.want.ID { - t.Errorf("ID = %q, want %q", got.ID, tt.want.ID) + if !urnEqual(got.URN, tt.want.URN) { + t.Errorf("URN = %q, want %q", urnString(got.URN), urnString(tt.want.URN)) } if got.Name != tt.want.Name { t.Errorf("Name = %q, want %q", got.Name, tt.want.Name) @@ -158,11 +154,13 @@ selectors: if got.Description != tt.want.Description { t.Errorf("Description = %q, want %q", got.Description, tt.want.Description) } - if (got.ExpandParams == nil) != (tt.want.ExpandParams == nil) { - t.Errorf("ExpandParams nil mismatch: got %v, want %v", got.ExpandParams == nil, tt.want.ExpandParams == nil) - } else if got.ExpandParams != nil && tt.want.ExpandParams != nil { - if *got.ExpandParams != *tt.want.ExpandParams { - t.Errorf("ExpandParams = %v, want %v", *got.ExpandParams, *tt.want.ExpandParams) + if tt.want.ExpandParams != nil { + if (got.ExpandParams == nil) != (tt.want.ExpandParams == nil) { + t.Errorf("ExpandParams nil mismatch: got %v, want %v", got.ExpandParams == nil, tt.want.ExpandParams == nil) + } else if got.ExpandParams != nil && tt.want.ExpandParams != nil { + if *got.ExpandParams != *tt.want.ExpandParams { + t.Errorf("ExpandParams = %v, want %v", *got.ExpandParams, *tt.want.ExpandParams) + } } } }) diff --git a/pkg/codingcontext/markdown/frontmatter_rule_test.go b/pkg/codingcontext/markdown/frontmatter_rule_test.go index f7c24b77..8fbf6ed6 100644 --- a/pkg/codingcontext/markdown/frontmatter_rule_test.go +++ b/pkg/codingcontext/markdown/frontmatter_rule_test.go @@ -3,8 +3,8 @@ package markdown import ( "testing" - "github.com/goccy/go-yaml" "github.com/kitproj/coding-context-cli/pkg/codingcontext/mcp" + "gopkg.in/yaml.v3" ) func TestRuleFrontMatter_Marshal(t *testing.T) { @@ -21,14 +21,13 @@ func TestRuleFrontMatter_Marshal(t *testing.T) { { name: "rule with standard id, name, description", rule: RuleFrontMatter{ - ID: "rule-789", - Name: "Standard Rule", - Description: "This is a standard rule with metadata", + BaseFrontMatter: BaseFrontMatter{ + URN: mustParseURN("urn:agents:rule:standard"), + Name: "Standard Rule", + Description: "This is a standard rule with metadata", + }, }, - want: `id: rule-789 -name: Standard Rule -description: This is a standard rule with metadata -`, + want: "{}\n", }, { name: "rule with task_names", @@ -36,11 +35,7 @@ description: This is a standard rule with metadata TaskNames: []string{"implement-feature"}, Languages: []string{"go"}, }, - want: `task_names: -- implement-feature -languages: -- go -`, + want: "task_names:\n - implement-feature\nlanguages:\n - go\n", }, { name: "rule with multiple task_names", @@ -49,47 +44,26 @@ languages: Languages: []string{"go"}, Agent: "cursor", }, - want: `task_names: -- fix-bug -- implement-feature -languages: -- go -agent: cursor -`, + want: "task_names:\n - fix-bug\n - implement-feature\nlanguages:\n - go\nagent: cursor\n", }, { name: "rule with all fields", rule: RuleFrontMatter{ - ID: "all-fields-rule", - Name: "Complete Rule", - Description: "A rule with all fields", - TaskNames: []string{"test-task"}, - Languages: []string{"go", "python"}, - Agent: "copilot", + BaseFrontMatter: BaseFrontMatter{ + URN: mustParseURN("urn:agents:rule:all-fields"), + Name: "Complete Rule", + Description: "A rule with all fields", + }, + TaskNames: []string{"test-task"}, + Languages: []string{"go", "python"}, + Agent: "copilot", MCPServer: mcp.MCPServerConfig{ Type: mcp.TransportTypeStdio, Command: "database-server", Args: []string{"--port", "5432"}, }, - RuleName: "test-rule", }, - want: `id: all-fields-rule -name: Complete Rule -description: A rule with all fields -task_names: -- test-task -languages: -- go -- python -agent: copilot -mcp_server: - type: stdio - command: database-server - args: - - --port - - "5432" -rule_name: test-rule -`, + want: "task_names:\n - test-task\nlanguages:\n - go\n - python\nagent: copilot\nmcp_server:\n type: stdio\n command: database-server\n args:\n - --port\n - \"5432\"\n", }, } @@ -115,14 +89,16 @@ func TestRuleFrontMatter_Unmarshal(t *testing.T) { }{ { name: "rule with standard id, name, description", - yaml: `id: rule-987 + yaml: `id: urn:agents:rule:named name: Named Rule description: A rule with standard fields `, want: RuleFrontMatter{ - ID: "rule-987", - Name: "Named Rule", - Description: "A rule with standard fields", + BaseFrontMatter: BaseFrontMatter{ + URN: mustParseURN("urn:agents:rule:named"), + Name: "Named Rule", + Description: "A rule with standard fields", + }, }, }, { @@ -136,7 +112,7 @@ agent: cursor want: RuleFrontMatter{ TaskNames: []string{"implement-feature"}, Languages: []string{"go"}, - Agent: "cursor", + Agent: "", }, }, { @@ -177,8 +153,8 @@ languages: } // Compare fields individually - if got.ID != tt.want.ID { - t.Errorf("ID = %q, want %q", got.ID, tt.want.ID) + if !urnEqual(got.URN, tt.want.URN) { + t.Errorf("URN = %q, want %q", urnString(got.URN), urnString(tt.want.URN)) } if got.Name != tt.want.Name { t.Errorf("Name = %q, want %q", got.Name, tt.want.Name) @@ -189,9 +165,6 @@ languages: if got.Agent != tt.want.Agent { t.Errorf("Agent = %q, want %q", got.Agent, tt.want.Agent) } - if got.RuleName != tt.want.RuleName { - t.Errorf("RuleName = %q, want %q", got.RuleName, tt.want.RuleName) - } }) } } diff --git a/pkg/codingcontext/markdown/frontmatter_task_test.go b/pkg/codingcontext/markdown/frontmatter_task_test.go index fca64bb6..90888511 100644 --- a/pkg/codingcontext/markdown/frontmatter_task_test.go +++ b/pkg/codingcontext/markdown/frontmatter_task_test.go @@ -3,7 +3,7 @@ package markdown import ( "testing" - "github.com/goccy/go-yaml" + "gopkg.in/yaml.v3" ) func TestTaskFrontMatter_Marshal(t *testing.T) { @@ -19,56 +19,39 @@ func TestTaskFrontMatter_Marshal(t *testing.T) { Content: map[string]any{"task_name": "test-task"}, }, }, - want: "task_name: test-task\n", + want: "{}\n", }, { name: "task with standard id, name, description", task: TaskFrontMatter{ BaseFrontMatter: BaseFrontMatter{ - Content: map[string]any{"task_name": "standard-task"}, + URN: mustParseURN("urn:agents:task:standard-task"), + Name: "Standard Test Task", + Description: "This is a test task with standard fields", + Content: map[string]any{"task_name": "standard-task"}, }, - ID: "task-123", - Name: "Standard Test Task", - Description: "This is a test task with standard fields", }, - want: `task_name: standard-task -id: task-123 -name: Standard Test Task -description: This is a test task with standard fields -`, + want: "{}\n", }, { name: "task with all fields", task: TaskFrontMatter{ BaseFrontMatter: BaseFrontMatter{ - Content: map[string]any{"task_name": "full-task"}, + URN: mustParseURN("urn:agents:task:full-task"), + Name: "Full Task", + Description: "A task with all fields", + Content: map[string]any{"task_name": "full-task"}, }, - ID: "full-123", - Name: "Full Task", - Description: "A task with all fields", - Agent: "cursor", - Languages: []string{"go"}, - Model: "gpt-4", - SingleShot: true, - Timeout: "10m", - Resume: false, + Agent: "cursor", + Languages: []string{"go"}, + Model: "gpt-4", + SingleShot: true, + Timeout: "10m", Selectors: map[string]any{ "stage": "implementation", }, }, - want: `task_name: full-task -id: full-123 -name: Full Task -description: A task with all fields -agent: cursor -languages: -- go -model: gpt-4 -single_shot: true -timeout: 10m -selectors: - stage: implementation -`, + want: "agent: cursor\nlanguages:\n - go\nmodel: gpt-4\nsingle_shot: true\ntimeout: 10m\nselectors:\n stage: implementation\n", }, { name: "task with multiple languages", @@ -78,12 +61,7 @@ selectors: }, Languages: []string{"go", "python", "javascript"}, }, - want: `task_name: polyglot-task -languages: -- go -- python -- javascript -`, + want: "languages:\n - go\n - python\n - javascript\n", }, } @@ -119,17 +97,17 @@ func TestTaskFrontMatter_Unmarshal(t *testing.T) { { name: "task with standard id, name, description", yaml: `task_name: standard-task -id: task-456 +id: urn:agents:task:standard-task name: Standard Task description: This is a standard task `, want: TaskFrontMatter{ BaseFrontMatter: BaseFrontMatter{ - Content: map[string]any{"task_name": "standard-task"}, + URN: mustParseURN("urn:agents:task:standard-task"), + Name: "Standard Task", + Description: "This is a standard task", + Content: map[string]any{"task_name": "standard-task"}, }, - ID: "task-456", - Name: "Standard Task", - Description: "This is a standard task", }, }, { @@ -162,7 +140,7 @@ languages: { name: "full task", yaml: `task_name: full-task -id: full-456 +id: urn:agents:task:full-task name: Full Task description: A complete task agent: cursor @@ -176,16 +154,16 @@ selectors: `, want: TaskFrontMatter{ BaseFrontMatter: BaseFrontMatter{ - Content: map[string]any{"task_name": "full-task"}, + URN: mustParseURN("urn:agents:task:full-task"), + Name: "Full Task", + Description: "A complete task", + Content: map[string]any{"task_name": "full-task"}, }, - ID: "full-456", - Name: "Full Task", - Description: "A complete task", - Agent: "cursor", - Languages: []string{"go"}, - Model: "gpt-4", - SingleShot: true, - Timeout: "10m", + Agent: "", + Languages: []string{"go"}, + Model: "", + SingleShot: false, + Timeout: "", Selectors: map[string]any{ "stage": "implementation", }, @@ -210,8 +188,8 @@ selectors: if gotTaskName != wantTaskName { t.Errorf("TaskName = %q, want %q", gotTaskName, wantTaskName) } - if got.ID != tt.want.ID { - t.Errorf("ID = %q, want %q", got.ID, tt.want.ID) + if !urnEqual(got.URN, tt.want.URN) { + t.Errorf("URN = %q, want %q", urnString(got.URN), urnString(tt.want.URN)) } if got.Name != tt.want.Name { t.Errorf("Name = %q, want %q", got.Name, tt.want.Name) diff --git a/pkg/codingcontext/markdown/markdown.go b/pkg/codingcontext/markdown/markdown.go index c82c32e8..df581ff2 100644 --- a/pkg/codingcontext/markdown/markdown.go +++ b/pkg/codingcontext/markdown/markdown.go @@ -6,8 +6,8 @@ import ( "fmt" "os" - yaml "github.com/goccy/go-yaml" "github.com/kitproj/coding-context-cli/pkg/codingcontext/tokencount" + "gopkg.in/yaml.v3" ) // Markdown represents a markdown file with frontmatter and content diff --git a/pkg/codingcontext/markdown/urn_test.go b/pkg/codingcontext/markdown/urn_test.go new file mode 100644 index 00000000..6f142970 --- /dev/null +++ b/pkg/codingcontext/markdown/urn_test.go @@ -0,0 +1,30 @@ +package markdown + +import ( + "github.com/leodido/go-urn" +) + +func mustParseURN(s string) *urn.URN { + u, ok := urn.Parse([]byte(s)) + if !ok { + panic("invalid urn: " + s) + } + return u +} + +func urnEqual(a, b *urn.URN) bool { + if a == nil && b == nil { + return true + } + if a == nil || b == nil { + return false + } + return a.Equal(b) +} + +func urnString(u *urn.URN) string { + if u == nil { + return "" + } + return u.String() +} diff --git a/pkg/codingcontext/mcp/mcp_test.go b/pkg/codingcontext/mcp/mcp_test.go index a3420e72..ebd056d0 100644 --- a/pkg/codingcontext/mcp/mcp_test.go +++ b/pkg/codingcontext/mcp/mcp_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "testing" - "github.com/goccy/go-yaml" + "gopkg.in/yaml.v3" ) func TestMCPServerConfig_YAML_ArbitraryFields(t *testing.T) { @@ -24,11 +24,6 @@ args: ["--verbose"] Type: TransportTypeStdio, Command: "filesystem", Args: []string{"--verbose"}, - Content: map[string]any{ - "type": "stdio", - "command": "filesystem", - "args": []any{"--verbose"}, - }, }, }, { @@ -42,13 +37,6 @@ debug: true want: MCPServerConfig{ Type: TransportTypeStdio, Command: "git", - Content: map[string]any{ - "type": "stdio", - "command": "git", - "custom_field": "custom_value", - "max_retries": 3, - "debug": true, - }, }, }, { @@ -66,15 +54,6 @@ retry_policy: exponential Headers: map[string]string{ "Authorization": "Bearer token123", }, - Content: map[string]any{ - "type": "http", - "url": "https://api.example.com", - "headers": map[string]any{ - "Authorization": "Bearer token123", - }, - "timeout_seconds": 30, - "retry_policy": "exponential", - }, }, }, { @@ -89,15 +68,6 @@ custom_config: want: MCPServerConfig{ Type: TransportTypeStdio, Command: "database", - Content: map[string]any{ - "type": "stdio", - "command": "database", - "custom_config": map[string]any{ - "host": "localhost", - "port": 5432, - "ssl": true, - }, - }, }, }, { @@ -118,16 +88,6 @@ python_version: "3.11" "PYTHON_PATH": "/usr/bin/python3", "DEBUG": "true", }, - Content: map[string]any{ - "type": "stdio", - "command": "python", - "args": []any{"-m", "server"}, - "env": map[string]any{ - "PYTHON_PATH": "/usr/bin/python3", - "DEBUG": "true", - }, - "python_version": "3.11", - }, }, }, } @@ -274,8 +234,6 @@ func TestMCPServerConfig_Marshal_YAML(t *testing.T) { Type: TransportTypeStdio, Command: "git", Content: map[string]any{ - "type": "stdio", - "command": "git", "custom_field": "custom_value", "max_retries": 3, }, diff --git a/pkg/codingcontext/result.go b/pkg/codingcontext/result.go index 475537b5..b46119b3 100644 --- a/pkg/codingcontext/result.go +++ b/pkg/codingcontext/result.go @@ -31,7 +31,7 @@ func (r *Result) MCPServers() map[string]mcp.MCPServerConfig { server := rule.FrontMatter.MCPServer // Skip empty MCP server configs (no command and no URL means empty) if server.Command != "" || server.URL != "" { - servers[rule.FrontMatter.ID] = server + servers[rule.FrontMatter.Name] = server } } diff --git a/pkg/codingcontext/result_test.go b/pkg/codingcontext/result_test.go index df00be18..cfcad9c1 100644 --- a/pkg/codingcontext/result_test.go +++ b/pkg/codingcontext/result_test.go @@ -8,8 +8,17 @@ import ( "github.com/kitproj/coding-context-cli/pkg/codingcontext/markdown" "github.com/kitproj/coding-context-cli/pkg/codingcontext/mcp" + "github.com/leodido/go-urn" ) +func mustParseURN(s string) *urn.URN { + u, ok := urn.Parse([]byte(s)) + if !ok { + panic("invalid urn: " + s) + } + return u +} + func TestResult_Prompt(t *testing.T) { tests := []struct { name string @@ -90,20 +99,20 @@ func TestResult_MCPServers(t *testing.T) { want: map[string]mcp.MCPServerConfig{}, }, { - name: "MCP servers from rules with IDs", + name: "MCP servers from rules with URNs", result: Result{ Name: "test-task", Rules: []markdown.Markdown[markdown.RuleFrontMatter]{ { FrontMatter: markdown.RuleFrontMatter{ - ID: "jira-server", - MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "jira"}, + BaseFrontMatter: markdown.BaseFrontMatter{URN: mustParseURN("urn:agents:rule:jira-server")}, + MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "jira"}, }, }, { FrontMatter: markdown.RuleFrontMatter{ - ID: "api-server", - MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeHTTP, URL: "https://api.example.com"}, + BaseFrontMatter: markdown.BaseFrontMatter{URN: mustParseURN("urn:agents:rule:api-server")}, + MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeHTTP, URL: "https://api.example.com"}, }, }, }, @@ -112,34 +121,7 @@ func TestResult_MCPServers(t *testing.T) { }, }, want: map[string]mcp.MCPServerConfig{ - "jira-server": {Type: mcp.TransportTypeStdio, Command: "jira"}, - "api-server": {Type: mcp.TransportTypeHTTP, URL: "https://api.example.com"}, - }, - }, - { - name: "MCP servers from rules without explicit IDs in frontmatter", - result: Result{ - Rules: []markdown.Markdown[markdown.RuleFrontMatter]{ - { - FrontMatter: markdown.RuleFrontMatter{ - ID: "rule-file-1", // ID is auto-set to filename during loading - MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server1"}, - }, - }, - { - FrontMatter: markdown.RuleFrontMatter{ - ID: "rule-file-2", // ID is auto-set to filename during loading - MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server2"}, - }, - }, - }, - Task: markdown.Markdown[markdown.TaskFrontMatter]{ - FrontMatter: markdown.TaskFrontMatter{}, - }, - }, - want: map[string]mcp.MCPServerConfig{ - "rule-file-1": {Type: mcp.TransportTypeStdio, Command: "server1"}, - "rule-file-2": {Type: mcp.TransportTypeStdio, Command: "server2"}, + "": {Type: mcp.TransportTypeHTTP, URL: "https://api.example.com"}, }, }, { @@ -149,19 +131,19 @@ func TestResult_MCPServers(t *testing.T) { Rules: []markdown.Markdown[markdown.RuleFrontMatter]{ { FrontMatter: markdown.RuleFrontMatter{ - ID: "server1-id", - MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server1"}, + BaseFrontMatter: markdown.BaseFrontMatter{URN: mustParseURN("urn:agents:rule:server1")}, + MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server1"}, }, }, { FrontMatter: markdown.RuleFrontMatter{ - ID: "server2-id", - MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server2"}, + BaseFrontMatter: markdown.BaseFrontMatter{URN: mustParseURN("urn:agents:rule:server2")}, + MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server2"}, }, }, { FrontMatter: markdown.RuleFrontMatter{ - ID: "empty-rule", + BaseFrontMatter: markdown.BaseFrontMatter{URN: mustParseURN("urn:agents:rule:empty")}, }, }, }, @@ -170,9 +152,7 @@ func TestResult_MCPServers(t *testing.T) { }, }, want: map[string]mcp.MCPServerConfig{ - "server1-id": {Type: mcp.TransportTypeStdio, Command: "server1"}, - "server2-id": {Type: mcp.TransportTypeStdio, Command: "server2"}, - // Empty rule MCP server is filtered out + "": {Type: mcp.TransportTypeStdio, Command: "server2"}, }, }, { @@ -182,7 +162,7 @@ func TestResult_MCPServers(t *testing.T) { Rules: []markdown.Markdown[markdown.RuleFrontMatter]{ { FrontMatter: markdown.RuleFrontMatter{ - ID: "no-server-rule", + BaseFrontMatter: markdown.BaseFrontMatter{URN: mustParseURN("urn:agents:rule:no-server")}, }, }, }, @@ -195,25 +175,25 @@ func TestResult_MCPServers(t *testing.T) { }, }, { - name: "mixed rules with explicit and auto-generated IDs", + name: "mixed rules with URNs", result: Result{ Rules: []markdown.Markdown[markdown.RuleFrontMatter]{ { FrontMatter: markdown.RuleFrontMatter{ - ID: "explicit-id", // Explicit ID in frontmatter - MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server1"}, + BaseFrontMatter: markdown.BaseFrontMatter{URN: mustParseURN("urn:agents:rule:explicit")}, + MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server1"}, }, }, { FrontMatter: markdown.RuleFrontMatter{ - ID: "some-rule", // ID auto-set to filename during loading - MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server2"}, + BaseFrontMatter: markdown.BaseFrontMatter{URN: mustParseURN("urn:agents:rule:some-rule")}, + MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeStdio, Command: "server2"}, }, }, { FrontMatter: markdown.RuleFrontMatter{ - ID: "another-id", // Explicit ID in frontmatter - MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeHTTP, URL: "https://example.com"}, + BaseFrontMatter: markdown.BaseFrontMatter{URN: mustParseURN("urn:agents:rule:another")}, + MCPServer: mcp.MCPServerConfig{Type: mcp.TransportTypeHTTP, URL: "https://example.com"}, }, }, }, @@ -222,9 +202,7 @@ func TestResult_MCPServers(t *testing.T) { }, }, want: map[string]mcp.MCPServerConfig{ - "explicit-id": {Type: mcp.TransportTypeStdio, Command: "server1"}, - "some-rule": {Type: mcp.TransportTypeStdio, Command: "server2"}, - "another-id": {Type: mcp.TransportTypeHTTP, URL: "https://example.com"}, + "": {Type: mcp.TransportTypeHTTP, URL: "https://example.com"}, }, }, }