Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.24.5
require (
github.com/alecthomas/participle/v2 v2.1.4
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
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,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=
Expand Down
15 changes: 8 additions & 7 deletions pkg/codingcontext/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,13 +387,14 @@ func TestContext_Run_Basic(t *testing.T) {
{
name: "task with explicit URN in frontmatter",
setup: func(t *testing.T, dir string) {
createTask(t, dir, "file-name", "id: urn:agents:task:file-name", "Task content")
createTask(t, dir, "file-name", "urn: urn:agents:task:file-name", "Task content")
},
taskName: "file-name",
wantErr: false,
check: func(t *testing.T, result *Result) {
if result.Task.FrontMatter.URN != "urn:agents:task:file-name" {
t.Errorf("expected task URN 'urn:agents:task:file-name', got %q", result.Task.FrontMatter.URN)
expectedURN := "urn:agents:task:file-name"
if result.Task.FrontMatter.URN == nil || result.Task.FrontMatter.URN.String() != expectedURN {
t.Errorf("expected task URN %q, got %q", expectedURN, result.Task.FrontMatter.URN)
}
},
},
Expand Down Expand Up @@ -762,8 +763,8 @@ func TestContext_Run_Rules(t *testing.T) {
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", "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")
createRule(t, dir, ".agents/rules/my-rule.md", "urn: urn:agents:rule:my-rule", "Rule with URN")
createRule(t, dir, ".agents/rules/another-rule.md", "urn: urn:agents:rule:another", "Rule with another URN")
},
taskName: "id-task",
wantErr: false,
Expand All @@ -775,13 +776,13 @@ func TestContext_Run_Rules(t *testing.T) {
foundMyRule := false
foundAnotherRule := false
for _, rule := range result.Rules {
if rule.FrontMatter.URN == "urn:agents:rule:my-rule" {
if rule.FrontMatter.URN != nil && rule.FrontMatter.URN.String() == "urn:agents:rule:my-rule" {
foundMyRule = true
if !strings.Contains(rule.Content, "Rule with URN") {
t.Error("my-rule should contain 'Rule with URN'")
}
}
if rule.FrontMatter.URN == "urn:agents:rule:another" {
if rule.FrontMatter.URN != nil && rule.FrontMatter.URN.String() == "urn:agents:rule:another" {
foundAnotherRule = true
if !strings.Contains(rule.Content, "Rule with another URN") {
t.Error("another should contain 'Rule with another URN'")
Expand Down
5 changes: 3 additions & 2 deletions pkg/codingcontext/markdown/frontmatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import (
"fmt"

"github.com/kitproj/coding-context-cli/pkg/codingcontext/mcp"
"github.com/kitproj/coding-context-cli/pkg/codingcontext/urn"
)

// 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:<name>)
// Automatically inferred from filename if not specified in frontmatter
URN string `yaml:"id,omitempty" json:"id,omitempty"`
// If not specified in frontmatter, this field remains empty and must be set explicitly by tooling if required.
URN *urn.URN `yaml:"urn,omitempty" json:"urn,omitempty"`
Comment on lines 13 to +15
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation in docs/reference/file-formats.md references "id" as the field name in multiple places (lines 32, 40, 486, 492, 652, 658), but this PR changes the field from "id" to "urn". The documentation should be updated to reflect the new field name and clarify that it expects URN format (e.g., "urn:agents:task:task-name").

Copilot uses AI. Check for mistakes.
Comment on lines 13 to +15
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example files in the examples directory still use the old "id:" field name instead of "urn:". These files should be updated to use the new field name and URN format. Affected files include:

  • examples/agents/commands/example-command-with-standard-fields.md (line 2)
  • examples/agents/rules/example-rule-with-standard-fields.md (line 2)
  • examples/agents/tasks/example-with-standard-fields.md (line 2)

Additionally, the content and comments in these example files reference "id" as the field name, which should be updated to "urn".

Copilot uses AI. Check for mistakes.

// Name is an optional human-readable name for the task
// Metadata only, does not affect task matching or filtering
Expand Down
42 changes: 27 additions & 15 deletions pkg/codingcontext/markdown/frontmatter_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@ package markdown
import (
"testing"

"github.com/kitproj/coding-context-cli/pkg/codingcontext/urn"
"gopkg.in/yaml.v3"
)

// mustParseURN is a test helper that parses a URN string and panics on error
func mustParseURNCommand(s string) *urn.URN {
u, ok := urn.Parse([]byte(s))
if !ok {
panic("failed to parse URN: " + s)
}
return u
}

func TestCommandFrontMatter_Marshal(t *testing.T) {
tests := []struct {
name string
Expand All @@ -21,12 +31,12 @@ func TestCommandFrontMatter_Marshal(t *testing.T) {
name: "command with standard id, name, description",
command: CommandFrontMatter{
BaseFrontMatter: BaseFrontMatter{
URN: "urn:agents:command:standard",
URN: mustParseURNCommand("urn:agents:command:standard"),
Name: "Standard Command",
Description: "This is a standard command with metadata",
},
},
want: `id: urn:agents:command:standard
want: `urn: urn:agents:command:standard
name: Standard Command
description: This is a standard command with metadata
`,
Expand All @@ -35,7 +45,7 @@ description: This is a standard command with metadata
name: "command with expand false",
command: CommandFrontMatter{
BaseFrontMatter: BaseFrontMatter{
URN: "urn:agents:command:no-expand",
URN: mustParseURNCommand("urn:agents:command:no-expand"),
Name: "No Expand Command",
Description: "Command with expansion disabled",
},
Expand All @@ -44,7 +54,7 @@ description: This is a standard command with metadata
return &b
}(),
},
want: `id: urn:agents:command:no-expand
want: `urn: urn:agents:command:no-expand
name: No Expand Command
description: Command with expansion disabled
expand: false
Expand All @@ -54,7 +64,7 @@ expand: false
name: "command with selectors",
command: CommandFrontMatter{
BaseFrontMatter: BaseFrontMatter{
URN: "urn:agents:command:selector",
URN: mustParseURNCommand("urn:agents:command:selector"),
Name: "Selector Command",
Description: "Command with selectors",
},
Expand All @@ -63,12 +73,12 @@ expand: false
"feature": "auth",
},
},
want: `id: urn:agents:command:selector
want: `urn: urn:agents:command:selector
name: Selector Command
description: Command with selectors
selectors:
database: postgres
feature: auth
database: postgres
feature: auth
`,
},
}
Expand All @@ -95,28 +105,28 @@ func TestCommandFrontMatter_Unmarshal(t *testing.T) {
}{
{
name: "command with standard id, name, description",
yaml: `id: urn:agents:command:named
yaml: `urn: urn:agents:command:named
name: Named Command
description: A command with standard fields
`,
want: CommandFrontMatter{
BaseFrontMatter: BaseFrontMatter{
URN: "urn:agents:command:named",
URN: mustParseURNCommand("urn:agents:command:named"),
Name: "Named Command",
Description: "A command with standard fields",
},
},
},
{
name: "command with expand false",
yaml: `id: urn:agents:command:no-expand
yaml: `urn: urn:agents:command:no-expand
name: No Expand
description: No expansion
expand: false
`,
want: CommandFrontMatter{
BaseFrontMatter: BaseFrontMatter{
URN: "urn:agents:command:no-expand",
URN: mustParseURNCommand("urn:agents:command:no-expand"),
Name: "No Expand",
Description: "No expansion",
},
Expand All @@ -128,7 +138,7 @@ expand: false
},
{
name: "command with selectors",
yaml: `id: urn:agents:command:selector
yaml: `urn: urn:agents:command:selector
name: Selector Command
description: Has selectors
selectors:
Expand All @@ -137,7 +147,7 @@ selectors:
`,
want: CommandFrontMatter{
BaseFrontMatter: BaseFrontMatter{
URN: "urn:agents:command:selector",
URN: mustParseURNCommand("urn:agents:command:selector"),
Name: "Selector Command",
Description: "Has selectors",
},
Expand All @@ -161,7 +171,9 @@ selectors:
}

// Compare fields individually
if got.URN != tt.want.URN {
if (got.URN == nil) != (tt.want.URN == nil) {
t.Errorf("URN = %v, want %v", got.URN, tt.want.URN)
} else if got.URN != nil && tt.want.URN != nil && !got.URN.Equal(tt.want.URN) {
t.Errorf("URN = %q, want %q", got.URN, tt.want.URN)
}
if got.Name != tt.want.Name {
Expand Down
26 changes: 19 additions & 7 deletions pkg/codingcontext/markdown/frontmatter_rule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@ import (
"testing"

"github.com/kitproj/coding-context-cli/pkg/codingcontext/mcp"
"github.com/kitproj/coding-context-cli/pkg/codingcontext/urn"
"gopkg.in/yaml.v3"
)

// mustParseURNRule is a test helper that parses a URN string and panics on error
func mustParseURNRule(s string) *urn.URN {
u, ok := urn.Parse([]byte(s))
if !ok {
panic("failed to parse URN: " + s)
}
return u
}

func TestRuleFrontMatter_Marshal(t *testing.T) {
tests := []struct {
name string
Expand All @@ -22,12 +32,12 @@ func TestRuleFrontMatter_Marshal(t *testing.T) {
name: "rule with standard id, name, description",
rule: RuleFrontMatter{
BaseFrontMatter: BaseFrontMatter{
URN: "urn:agents:rule:standard",
URN: mustParseURNRule("urn:agents:rule:standard"),
Name: "Standard Rule",
Description: "This is a standard rule with metadata",
},
},
want: `id: urn:agents:rule:standard
want: `urn: urn:agents:rule:standard
name: Standard Rule
description: This is a standard rule with metadata
`,
Expand Down Expand Up @@ -63,7 +73,7 @@ agent: cursor
name: "rule with all fields",
rule: RuleFrontMatter{
BaseFrontMatter: BaseFrontMatter{
URN: "urn:agents:rule:all-fields",
URN: mustParseURNRule("urn:agents:rule:all-fields"),
Name: "Complete Rule",
Description: "A rule with all fields",
},
Expand All @@ -77,7 +87,7 @@ agent: cursor
},
RuleName: "test-rule",
},
want: `id: urn:agents:rule:all-fields
want: `urn: urn:agents:rule:all-fields
name: Complete Rule
description: A rule with all fields
task_names:
Expand Down Expand Up @@ -119,13 +129,13 @@ func TestRuleFrontMatter_Unmarshal(t *testing.T) {
}{
{
name: "rule with standard id, name, description",
yaml: `id: urn:agents:rule:named
yaml: `urn: urn:agents:rule:named
name: Named Rule
description: A rule with standard fields
`,
want: RuleFrontMatter{
BaseFrontMatter: BaseFrontMatter{
URN: "urn:agents:rule:named",
URN: mustParseURNRule("urn:agents:rule:named"),
Name: "Named Rule",
Description: "A rule with standard fields",
},
Expand Down Expand Up @@ -183,7 +193,9 @@ languages:
}

// Compare fields individually
if got.URN != tt.want.URN {
if (got.URN == nil) != (tt.want.URN == nil) {
t.Errorf("URN = %v, want %v", got.URN, tt.want.URN)
} else if got.URN != nil && tt.want.URN != nil && !got.URN.Equal(tt.want.URN) {
t.Errorf("URN = %q, want %q", got.URN, tt.want.URN)
}
if got.Name != tt.want.Name {
Expand Down
31 changes: 22 additions & 9 deletions pkg/codingcontext/markdown/frontmatter_task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@ package markdown
import (
"testing"

"github.com/kitproj/coding-context-cli/pkg/codingcontext/urn"
"gopkg.in/yaml.v3"
)

// mustParseURN is a test helper that parses a URN string and panics on error
func mustParseURN(s string) *urn.URN {
u, ok := urn.Parse([]byte(s))
if !ok {
panic("failed to parse URN: " + s)
}
return u
}

func TestTaskFrontMatter_Marshal(t *testing.T) {
tests := []struct {
name string
Expand All @@ -25,14 +35,14 @@ func TestTaskFrontMatter_Marshal(t *testing.T) {
name: "task with standard id, name, description",
task: TaskFrontMatter{
BaseFrontMatter: BaseFrontMatter{
URN: "urn:agents:task: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"},
},
},
want: `task_name: standard-task
id: urn:agents:task:standard-task
urn: urn:agents:task:standard-task
name: Standard Test Task
description: This is a test task with standard fields
`,
Expand All @@ -41,7 +51,7 @@ description: This is a test task with standard fields
name: "task with all fields",
task: TaskFrontMatter{
BaseFrontMatter: BaseFrontMatter{
URN: "urn:agents:task: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"},
Expand All @@ -56,7 +66,7 @@ description: This is a test task with standard fields
},
},
want: `task_name: full-task
id: urn:agents:task:full-task
urn: urn:agents:task:full-task
name: Full Task
description: A task with all fields
agent: cursor
Expand Down Expand Up @@ -118,13 +128,13 @@ func TestTaskFrontMatter_Unmarshal(t *testing.T) {
{
name: "task with standard id, name, description",
yaml: `task_name: standard-task
id: urn:agents:task:standard-task
urn: urn:agents:task:standard-task
name: Standard Task
description: This is a standard task
`,
want: TaskFrontMatter{
BaseFrontMatter: BaseFrontMatter{
URN: "urn:agents:task: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"},
Expand Down Expand Up @@ -161,7 +171,7 @@ languages:
{
name: "full task",
yaml: `task_name: full-task
id: urn:agents:task:full-task
urn: urn:agents:task:full-task
name: Full Task
description: A complete task
agent: cursor
Expand All @@ -175,7 +185,7 @@ selectors:
`,
want: TaskFrontMatter{
BaseFrontMatter: BaseFrontMatter{
URN: "urn:agents:task:full-task",
URN: mustParseURN("urn:agents:task:full-task"),
Name: "Full Task",
Description: "A complete task",
Content: map[string]any{"task_name": "full-task"},
Expand Down Expand Up @@ -209,7 +219,10 @@ selectors:
if gotTaskName != wantTaskName {
t.Errorf("TaskName = %q, want %q", gotTaskName, wantTaskName)
}
if got.URN != tt.want.URN {
// Compare URNs
if (got.URN == nil) != (tt.want.URN == nil) {
t.Errorf("URN = %v, want %v", got.URN, tt.want.URN)
} else if got.URN != nil && tt.want.URN != nil && !got.URN.Equal(tt.want.URN) {
t.Errorf("URN = %q, want %q", got.URN, tt.want.URN)
}
if got.Name != tt.want.Name {
Expand Down
Loading