Skip to content

Commit a315a3c

Browse files
Copilotalexec
andauthored
Add selector support to command frontmatter (#168)
* Initial plan * Add support for command frontmatter selectors - Added Selectors field to CommandFrontMatter struct - Updated findCommand to extract and merge command selectors with task selectors - Added table-driven tests for command selectors functionality - Created example command file with selectors Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> * Update documentation for command selectors feature - Added documentation for selectors field in command frontmatter - Explained how command selectors combine with task selectors - Provided examples of using command selectors Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> * Refactor selector merging to reduce code duplication - Extracted mergeSelectors helper function - Updated task and command selector handling to use the helper - Addresses code review feedback Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexec <1142830+alexec@users.noreply.github.com>
1 parent c458584 commit a315a3c

5 files changed

Lines changed: 155 additions & 11 deletions

File tree

docs/reference/file-formats.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,46 @@ coding-context -p environment=prod -p version=1.0 my-task
468468

469469
This is useful when commands contain template syntax that should be preserved.
470470

471+
#### `selectors` (optional)
472+
473+
**Type:** Map of key-value pairs
474+
**Purpose:** Specifies selectors that filter which rules are included when this command is used. Command selectors are combined with task selectors and CLI selectors using OR logic.
475+
476+
When a task uses a command that has selectors, those selectors are merged with the task's selectors to filter rules. This allows commands to specify which rules they need (e.g., database-specific rules, authentication rules, etc.).
477+
478+
**Example:**
479+
```yaml
480+
---
481+
selectors:
482+
database: postgres
483+
feature: auth
484+
---
485+
486+
# Database Setup Instructions
487+
488+
This command provides database setup instructions.
489+
```
490+
491+
**Usage in a task:**
492+
```yaml
493+
---
494+
selectors:
495+
env: production
496+
---
497+
498+
Deploy to production environment.
499+
500+
/setup-database
501+
```
502+
503+
When this task runs:
504+
- Rules with `env: production` will be included (from task selector)
505+
- Rules with `database: postgres` will be included (from command selector)
506+
- Rules with `feature: auth` will be included (from command selector)
507+
- Rules that match any of these selectors will be included (OR logic)
508+
509+
This allows commands to declare their dependencies on specific rules without requiring every task to manually specify them.
510+
471511
### Slash Command Syntax
472512

473513
Commands are referenced from tasks using slash command syntax:
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
selectors:
3+
database: postgres
4+
feature: auth
5+
---
6+
7+
# Database Setup Instructions
8+
9+
This command provides database setup instructions. When this command is used in a task,
10+
it will automatically include rules that are tagged with `database: postgres` and
11+
`feature: auth` in their frontmatter.
12+
13+
## PostgreSQL Configuration
14+
15+
Connect to PostgreSQL:
16+
```bash
17+
psql -U ${db_user} -d ${db_name}
18+
```
19+
20+
## Authentication Setup
21+
22+
Configure authentication tables and initial data.

pkg/codingcontext/context.go

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,7 @@ func (cc *Context) findTask(taskName string) error {
126126
// rules match if their frontmatter value matches ANY selector value for a given key.
127127
// For example: if CLI has env=development and task has env=production,
128128
// rules with either env=development OR env=production will be included.
129-
for key, value := range frontMatter.Selectors {
130-
switch v := value.(type) {
131-
case []any:
132-
for _, item := range v {
133-
cc.includes.SetValue(key, fmt.Sprint(item))
134-
}
135-
default:
136-
cc.includes.SetValue(key, fmt.Sprint(v))
137-
}
138-
}
129+
cc.mergeSelectors(frontMatter.Selectors)
139130

140131
// Task frontmatter agent field overrides -a flag
141132
if frontMatter.Agent != "" {
@@ -210,9 +201,11 @@ func (cc *Context) findTask(taskName string) error {
210201
}
211202

212203
// findCommand searches for a command markdown file and returns its content.
213-
// Commands now support optional frontmatter with the expand field.
204+
// Commands now support optional frontmatter with the expand field and selectors.
214205
// Parameters are substituted by default (when expand is nil or true).
215206
// Substitution is skipped only when expand is explicitly set to false.
207+
// If the command has selectors in its frontmatter, they are merged into cc.includes
208+
// to allow commands to specify which rules they need.
216209
func (cc *Context) findCommand(commandName string, params taskparser.Params) (string, error) {
217210
var content *string
218211
err := cc.visitMarkdownFiles(commandSearchPaths, func(path string) error {
@@ -228,6 +221,11 @@ func (cc *Context) findCommand(commandName string, params taskparser.Params) (st
228221
return err
229222
}
230223

224+
// Extract selector labels from command frontmatter and add them to cc.includes.
225+
// This combines CLI selectors, task selectors, and command selectors using OR logic:
226+
// rules match if their frontmatter value matches ANY selector value for a given key.
227+
cc.mergeSelectors(frontMatter.Selectors)
228+
231229
// Expand parameters only if expand is not explicitly set to false
232230
var processedContent string
233231
if shouldExpandParams(frontMatter.ExpandParams) {
@@ -251,6 +249,22 @@ func (cc *Context) findCommand(commandName string, params taskparser.Params) (st
251249
return *content, nil
252250
}
253251

252+
// mergeSelectors adds selectors from a map into cc.includes.
253+
// This is used to combine selectors from task and command frontmatter with CLI selectors.
254+
// The merge uses OR logic: rules match if their frontmatter value matches ANY selector value for a given key.
255+
func (cc *Context) mergeSelectors(selectors map[string]any) {
256+
for key, value := range selectors {
257+
switch v := value.(type) {
258+
case []any:
259+
for _, item := range v {
260+
cc.includes.SetValue(key, fmt.Sprint(item))
261+
}
262+
default:
263+
cc.includes.SetValue(key, fmt.Sprint(v))
264+
}
265+
}
266+
}
267+
254268
// expandParams performs all types of content expansion:
255269
// - Parameter expansion: ${param_name}
256270
// - Command expansion: !`command`

pkg/codingcontext/context_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -867,6 +867,70 @@ func TestContext_Run_Commands(t *testing.T) {
867867
}
868868
},
869869
},
870+
{
871+
name: "command with selectors filters rules",
872+
setup: func(t *testing.T, dir string) {
873+
// Task uses a command that has selectors
874+
createTask(t, dir, "task-with-cmd", "", "/setup-db")
875+
// Command has selectors that should be applied to rule filtering
876+
createCommand(t, dir, "setup-db", "selectors:\n database: postgres", "Setting up database...")
877+
// Rules with different database values
878+
createRule(t, dir, ".agents/rules/postgres-rule.md", "database: postgres", "PostgreSQL rule")
879+
createRule(t, dir, ".agents/rules/mysql-rule.md", "database: mysql", "MySQL rule")
880+
createRule(t, dir, ".agents/rules/generic-rule.md", "", "Generic rule")
881+
},
882+
taskName: "task-with-cmd",
883+
wantErr: false,
884+
check: func(t *testing.T, result *Result) {
885+
// Should include postgres-rule and generic-rule
886+
// Should exclude mysql-rule
887+
if len(result.Rules) != 2 {
888+
t.Errorf("expected 2 rules, got %d", len(result.Rules))
889+
}
890+
foundPostgres := false
891+
foundMySQL := false
892+
for _, rule := range result.Rules {
893+
if strings.Contains(rule.Content, "PostgreSQL rule") {
894+
foundPostgres = true
895+
}
896+
if strings.Contains(rule.Content, "MySQL rule") {
897+
foundMySQL = true
898+
}
899+
}
900+
if !foundPostgres {
901+
t.Error("expected to find PostgreSQL rule")
902+
}
903+
if foundMySQL {
904+
t.Error("did not expect to find MySQL rule")
905+
}
906+
},
907+
},
908+
{
909+
name: "command selectors combine with task selectors",
910+
setup: func(t *testing.T, dir string) {
911+
// Task has its own selectors
912+
createTask(t, dir, "combined-selectors", "selectors:\n env: production", "/enable-feature")
913+
// Command also has selectors
914+
createCommand(t, dir, "enable-feature", "selectors:\n feature: auth", "Enabling authentication...")
915+
// Rules with different combinations
916+
createRule(t, dir, ".agents/rules/prod-auth-rule.md", "env: production\nfeature: auth", "Production auth rule")
917+
createRule(t, dir, ".agents/rules/prod-rule.md", "env: production", "Production rule")
918+
createRule(t, dir, ".agents/rules/auth-rule.md", "feature: auth", "Auth rule")
919+
createRule(t, dir, ".agents/rules/dev-rule.md", "env: development", "Development rule")
920+
},
921+
taskName: "combined-selectors",
922+
wantErr: false,
923+
check: func(t *testing.T, result *Result) {
924+
// Should include: prod-auth-rule (matches both), prod-rule (matches env), auth-rule (matches feature)
925+
// Should exclude: dev-rule (env doesn't match)
926+
if len(result.Rules) != 3 {
927+
t.Errorf("expected 3 rules, got %d", len(result.Rules))
928+
for _, r := range result.Rules {
929+
t.Logf("Found rule: %s", r.Content)
930+
}
931+
}
932+
},
933+
},
870934
}
871935

872936
for _, tt := range tests {

pkg/codingcontext/markdown/frontmatter.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ type CommandFrontMatter struct {
7777
// ExpandParams controls whether parameter expansion should occur
7878
// Defaults to true if not specified
7979
ExpandParams *bool `yaml:"expand,omitempty" json:"expand,omitempty"`
80+
81+
// Selectors contains additional custom selectors for filtering rules
82+
// When a command is used in a task, its selectors are combined with task selectors
83+
Selectors map[string]any `yaml:"selectors,omitempty" json:"selectors,omitempty"`
8084
}
8185

8286
// UnmarshalJSON custom unmarshaler that populates both typed fields and Content map

0 commit comments

Comments
 (0)