Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
81700ff
Initial plan
Copilot Dec 6, 2025
3cbeb89
Implement enhanced task parser with participle v2
Copilot Dec 6, 2025
cdfc17f
Update context.go to use new parser and delete old slashcommand.go
Copilot Dec 6, 2025
61b188f
Address code review comments - improve documentation
Copilot Dec 6, 2025
ccaa967
Process all task blocks instead of just first slash command
Copilot Dec 7, 2025
b54bbd9
Extract selector extraction logic into helper method
Copilot Dec 7, 2025
8c7e925
Add SlashCommand.Params() method and use first task frontmatter only
Copilot Dec 7, 2025
5994d82
Revert all changes to context.go
Copilot Dec 7, 2025
47be16a
Separate tasks and commands, add @ prefix support and new API
Copilot Dec 7, 2025
9dc23f5
Refactor to separate tasks and commands with new getMarkdown/getTask/…
Copilot Dec 7, 2025
c0fe86d
Add CommandFrontMatter type and refactor to use task name lookup
Copilot Dec 7, 2025
b635dbf
Reinstate getMarkdown with proper signature, remove CommandFrontMatte…
Copilot Dec 7, 2025
2b8f786
Add CommandFrontMatter struct instead of using nil
Copilot Dec 7, 2025
695731a
Simplify getMarkdown by extracting helper functions
Copilot Dec 7, 2025
e5e9b5d
Add selector filtering to findMarkdownFile
Copilot Dec 7, 2025
74654a4
Build successfully, tests partially passing - free-text prompts work
Copilot Dec 8, 2025
672fe3f
Changes before error encountered
Copilot Dec 8, 2025
72a990c
ok
alexec Dec 8, 2025
9b098fa
Add unit tests for coding context functionality
alexec Dec 8, 2025
a26476e
Refactor integration tests and coding context
alexec Dec 8, 2025
a78b95c
Update pkg/codingcontext/context.go
alexec Dec 8, 2025
bdbf7d8
Update pkg/codingcontext/context.go
alexec Dec 8, 2025
9317374
Update pkg/codingcontext/context.go
alexec Dec 8, 2025
59097cf
Update pkg/codingcontext/context.go
alexec Dec 8, 2025
da60f3d
Update pkg/codingcontext/context.go
alexec Dec 8, 2025
04c486a
Enhance error handling in findTask method
alexec Dec 9, 2025
9a27cfc
Update tests to expect errors for missing/empty tasks
Copilot Dec 9, 2025
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 @@ -3,6 +3,7 @@ module github.com/kitproj/coding-context-cli
go 1.24.4

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
)
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U=
github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
Expand All @@ -16,6 +22,8 @@ github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhE
github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I=
github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/klauspost/compress v1.11.2 h1:MiK62aErc3gIiVEtyzKfeOHgW7atJb5g/KNX5m3c2nQ=
github.com/klauspost/compress v1.11.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
Expand Down
180 changes: 35 additions & 145 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ echo "Running bootstrap"
createStandardTask(t, dirs.tasksDir, "test-task")

// Run the program
output := runTool(t, "-C", dirs.tmpDir, "/test-task")
output := runTool(t, "-C", dirs.tmpDir, "test-task")

// Check that bootstrap output appears before rule content
bootstrapIdx := strings.Index(output, "Running bootstrap")
Expand Down Expand Up @@ -142,7 +142,7 @@ General information about the project.
createStandardTask(t, dirs.tasksDir, "test-task")

// Run the program - should succeed without a bootstrap file
output := runTool(t, "-C", dirs.tmpDir, "/test-task")
output := runTool(t, "-C", dirs.tmpDir, "test-task")

// Check that rule content is present
if !strings.Contains(output, "# Project Info") {
Expand Down Expand Up @@ -201,7 +201,7 @@ echo "Running deploy bootstrap"
createStandardTask(t, dirs.tasksDir, "test-task")

// Run the program
output := runTool(t, "-C", dirs.tmpDir, "/test-task")
output := runTool(t, "-C", dirs.tmpDir, "test-task")

// Check that both bootstrap scripts ran
if !strings.Contains(output, "Running setup bootstrap") {
Expand Down Expand Up @@ -251,7 +251,7 @@ Go specific guidelines.
createStandardTask(t, dirs.tasksDir, "test-task")

// Run the program with selector filtering for Python
output := runTool(t, "-C", dirs.tmpDir, "-s", "language=python", "/test-task")
output := runTool(t, "-C", dirs.tmpDir, "-s", "language=python", "test-task")

// Check that only Python guidelines are included
if !strings.Contains(output, "# Python Guidelines") {
Expand Down Expand Up @@ -284,7 +284,7 @@ Please work on ${component} and fix ${issue}.
}

// Run the program with parameters
output := runTool(t, "-C", tmpDir, "-p", "component=auth", "-p", "issue=login bug", "/test-task")
output := runTool(t, "-C", tmpDir, "-p", "component=auth", "-p", "issue=login bug", "test-task")

// Check that template variables were expanded
if !strings.Contains(output, "Please work on auth and fix login bug.") {
Expand All @@ -310,7 +310,7 @@ This is a .mdc file.
createStandardTask(t, dirs.tasksDir, "test-task")

// Run the program
output := runTool(t, "-C", dirs.tmpDir, "/test-task")
output := runTool(t, "-C", dirs.tmpDir, "test-task")

// Check that .mdc file content is present
if !strings.Contains(output, "# Custom Rules") {
Expand Down Expand Up @@ -345,7 +345,7 @@ echo "Running custom bootstrap"
createStandardTask(t, dirs.tasksDir, "test-task")

// Run the program
output := runTool(t, "-C", dirs.tmpDir, "/test-task")
output := runTool(t, "-C", dirs.tmpDir, "test-task")

// Check that bootstrap ran and content is present
if !strings.Contains(output, "Running custom bootstrap") {
Expand Down Expand Up @@ -394,7 +394,7 @@ echo "Bootstrap executed successfully"
createStandardTask(t, dirs.tasksDir, "test-task")

// Run the program - this should chmod +x the bootstrap file before running it
output := runTool(t, "-C", dirs.tmpDir, "/test-task")
output := runTool(t, "-C", dirs.tmpDir, "test-task")

// Check that bootstrap output appears (proving it ran successfully)
if !strings.Contains(output, "Bootstrap executed successfully") {
Expand Down Expand Up @@ -457,7 +457,7 @@ This is a test task.
}

// Run the program
output := runTool(t, "-C", tmpDir, "/test-opencode")
output := runTool(t, "-C", tmpDir, "test-opencode")

// Check that agent rule content is present
if !strings.Contains(output, "# Documentation Agent") {
Expand All @@ -472,34 +472,35 @@ This is a test task.

func TestOpenCodeCommandTaskSupport(t *testing.T) {
tmpDir := t.TempDir()
openCodeCommandDir := filepath.Join(tmpDir, ".opencode", "command")
// Tasks must be in .agents/tasks directory
tasksDir := filepath.Join(tmpDir, ".agents", "tasks")

if err := os.MkdirAll(openCodeCommandDir, 0o755); err != nil {
t.Fatalf("failed to create opencode command dir: %v", err)
if err := os.MkdirAll(tasksDir, 0o755); err != nil {
t.Fatalf("failed to create tasks dir: %v", err)
}

// Create a task file in .opencode/command
taskFile := filepath.Join(openCodeCommandDir, "fix-bug.md")
// Create a task file in the correct location
taskFile := filepath.Join(tasksDir, "fix-bug.md")
taskContent := `---
task_name: fix-bug
---
# Fix Bug Command
# Fix Bug Task

This is an OpenCode command task for fixing bugs.
This is a task for fixing bugs.
`
if err := os.WriteFile(taskFile, []byte(taskContent), 0o644); err != nil {
t.Fatalf("failed to write task file: %v", err)
}

// Run the program
output := runTool(t, "-C", tmpDir, "/fix-bug")
output := runTool(t, "-C", tmpDir, "fix-bug")

// Check that task content is present
if !strings.Contains(output, "# Fix Bug Command") {
t.Errorf("OpenCode command task content not found in stdout")
if !strings.Contains(output, "# Fix Bug Task") {
t.Errorf("task content not found in stdout")
}
if !strings.Contains(output, "This is an OpenCode command task for fixing bugs.") {
t.Errorf("OpenCode command task description not found in stdout")
if !strings.Contains(output, "This is a task for fixing bugs.") {
t.Errorf("task description not found in stdout")
}
}

Expand All @@ -524,7 +525,7 @@ This task name is based on the filename.
}

// Run the program with task name matching the filename
output := runTool(t, "-C", tmpDir, "/my-special-task")
output := runTool(t, "-C", tmpDir, "my-special-task")

// Check that task content is present
if !strings.Contains(output, "# My Special Task") {
Expand Down Expand Up @@ -554,7 +555,7 @@ This file uses the filename as task_name.
}

// Run the program - should succeed using filename as task name
output := runTool(t, "-C", tmpDir, "/my-task")
output := runTool(t, "-C", tmpDir, "my-task")

// Check that task content is present
if !strings.Contains(output, "# My Task") {
Expand All @@ -565,54 +566,6 @@ This file uses the filename as task_name.
}
}

func TestMultipleTasksWithSameNameError(t *testing.T) {
tmpDir := t.TempDir()
tasksDir := filepath.Join(tmpDir, ".agents", "tasks")

if err := os.MkdirAll(tasksDir, 0o755); err != nil {
t.Fatalf("failed to create tasks dir: %v", err)
}

// Create two task files with the SAME filename in different directories
taskFile1 := filepath.Join(tasksDir, "duplicate-task.md")
taskContent1 := `---
---
# Task File 1

This is the first file.
`
if err := os.WriteFile(taskFile1, []byte(taskContent1), 0o644); err != nil {
t.Fatalf("failed to write task file 1: %v", err)
}

// Create another file with same name in a different search directory
cursorDir := filepath.Join(tmpDir, ".cursor", "commands")
if err := os.MkdirAll(cursorDir, 0o755); err != nil {
t.Fatalf("failed to create cursor dir: %v", err)
}
taskFile2 := filepath.Join(cursorDir, "duplicate-task.md")
taskContent2 := `---
---
# Task File 2

This is the second file.
`
if err := os.WriteFile(taskFile2, []byte(taskContent2), 0o644); err != nil {
t.Fatalf("failed to write task file 2: %v", err)
}

// Run the program - should fail with an error about duplicate task names
output, err := runToolWithError("-C", tmpDir, "/duplicate-task")
if err == nil {
t.Fatalf("expected program to fail with duplicate task names, but it succeeded")
}

// Check that error message mentions multiple task files
if !strings.Contains(output, "multiple task files found") {
t.Errorf("expected error about multiple task files, got: %s", output)
}
}

func TestTaskSelectionWithSelectors(t *testing.T) {
tmpDir := t.TempDir()
tasksDir := filepath.Join(tmpDir, ".agents", "tasks")
Expand Down Expand Up @@ -647,7 +600,7 @@ Deploy to the production environment.
}

// Run the program with selector for staging - use the staging task filename
output := runTool(t, "-C", tmpDir, "-s", "environment=staging", "/deploy-staging")
output := runTool(t, "-C", tmpDir, "-s", "environment=staging", "deploy-staging")

// Check that staging task content is present
if !strings.Contains(output, "# Deploy to Staging") {
Expand All @@ -658,7 +611,7 @@ Deploy to the production environment.
}

// Run the program with selector for production - use the production task filename
output = runTool(t, "-C", tmpDir, "-s", "environment=production", "/deploy-production")
output = runTool(t, "-C", tmpDir, "-s", "environment=production", "deploy-production")

// Check that production task content is present
if !strings.Contains(output, "# Deploy to Production") {
Expand Down Expand Up @@ -724,7 +677,7 @@ This is the resume task prompt for continuing the bug fix.
if err != nil {
t.Fatalf("failed to get working directory: %v", err)
}
cmd := exec.Command("go", "run", wd, "-C", dirs.tmpDir, "-s", "resume=false", "/fix-bug")
cmd := exec.Command("go", "run", wd, "-C", dirs.tmpDir, "-s", "resume=false", "fix-bug")
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
Expand Down Expand Up @@ -754,7 +707,7 @@ This is the resume task prompt for continuing the bug fix.

// Test 2: Run in resume mode (with -s resume=true selector)
// Capture stdout and stderr separately to verify bootstrap scripts don't run
cmd = exec.Command("go", "run", wd, "-C", dirs.tmpDir, "-s", "resume=true", "/fix-bug-resume")
cmd = exec.Command("go", "run", wd, "-C", dirs.tmpDir, "-s", "resume=true", "fix-bug-resume")
stdout.Reset()
stderr.Reset()
cmd.Stdout = &stdout
Expand Down Expand Up @@ -784,7 +737,7 @@ This is the resume task prompt for continuing the bug fix.
}

// Test 3: Run in resume mode (with -r flag)
cmd = exec.Command("go", "run", wd, "-C", dirs.tmpDir, "-r", "/fix-bug-resume")
cmd = exec.Command("go", "run", wd, "-C", dirs.tmpDir, "-r", "fix-bug-resume")
stdout.Reset()
stderr.Reset()
cmd.Stdout = &stdout
Expand Down Expand Up @@ -846,7 +799,7 @@ This is a rule loaded from a remote directory.

// Run the program with remote directory (using file:// URL)
remoteURL := "file://" + remoteDir
output := runTool(t, "-C", tmpDir, "-d", remoteURL, "/test-task")
output := runTool(t, "-C", tmpDir, "-d", remoteURL, "test-task")

// Check that remote rule content is present
if !strings.Contains(output, "# Remote Rule") {
Expand Down Expand Up @@ -894,7 +847,7 @@ This is a test task.
}

// Test that frontmatter is always printed
output := runTool(t, "-C", dirs.tmpDir, "/test-task")
output := runTool(t, "-C", dirs.tmpDir, "test-task")

lines := strings.Split(output, "\n")

Expand Down Expand Up @@ -986,7 +939,7 @@ This is a test task.
// Note: Tasks no longer have bootstrap scripts - only rules do

// Run the program
output := runTool(t, "-C", dirs.tmpDir, "/test-task")
output := runTool(t, "-C", dirs.tmpDir, "test-task")

// Check that task content is present
if !strings.Contains(output, "# Test Task") {
Expand All @@ -1011,7 +964,7 @@ This task has no bootstrap script.
}

// Run the program - should succeed without a bootstrap file
output := runTool(t, "-C", dirs.tmpDir, "/no-bootstrap-task")
output := runTool(t, "-C", dirs.tmpDir, "no-bootstrap-task")

// Check that task content is present
if !strings.Contains(output, "# Task Without Bootstrap") {
Expand Down Expand Up @@ -1056,7 +1009,7 @@ Deploy instructions.
}

// Run the program
output := runTool(t, "-C", dirs.tmpDir, "/deploy-task")
output := runTool(t, "-C", dirs.tmpDir, "deploy-task")

// Check that rule bootstrap ran (rules still have bootstrap scripts)
if !strings.Contains(output, "Running rule bootstrap") {
Expand Down Expand Up @@ -1084,69 +1037,6 @@ Deploy instructions.
}
}

func TestFreeTextPromptWithRules(t *testing.T) {
dirs := setupTestDirs(t)

// Create a rule file that should be included
ruleFile := filepath.Join(dirs.rulesDir, "coding-standards.md")
ruleContent := `---
---
# Coding Standards

Follow these coding standards.
`
if err := os.WriteFile(ruleFile, []byte(ruleContent), 0o644); err != nil {
t.Fatalf("failed to write rule file: %v", err)
}

// Run with a free-text prompt (no slash command)
output := runTool(t, "-C", dirs.tmpDir, "Please help me fix a bug in the login system")

// Free-text prompt should be output as task content
if !strings.Contains(output, "Please help me fix a bug in the login system") {
t.Errorf("free-text prompt not found in stdout")
}

// Rules should still be included
if !strings.Contains(output, "# Coding Standards") {
t.Errorf("rule content not found in stdout")
}

// Test with parameter expansion (combined into same test)
output = runTool(t, "-C", dirs.tmpDir, "-p", "component=auth", "Please work on ${component}")
if !strings.Contains(output, "Please work on auth") {
t.Errorf("parameter expansion not working correctly. Output:\n%s", output)
}
}

func TestSlashCommandWithArguments(t *testing.T) {
dirs := setupTestDirs(t)

// Create a task file that uses positional arguments
taskFile := filepath.Join(dirs.tasksDir, "fix-issue.md")
taskContent := `---
task_name: fix-issue
---
# Fix Issue ${1}

Please fix issue number ${1} with priority ${2}.
`
if err := os.WriteFile(taskFile, []byte(taskContent), 0o644); err != nil {
t.Fatalf("failed to write task file: %v", err)
}

// Run with a slash command that includes arguments
output := runTool(t, "-C", dirs.tmpDir, "/fix-issue 123 high")

// Arguments should be substituted into the task content
if !strings.Contains(output, "# Fix Issue 123") {
t.Errorf("positional argument ${1} not substituted in title. Output:\n%s", output)
}
if !strings.Contains(output, "Please fix issue number 123 with priority high.") {
t.Errorf("positional arguments not substituted in content. Output:\n%s", output)
}
}

func TestManifestFile(t *testing.T) {
// Create main project directory
mainDir := t.TempDir()
Expand Down Expand Up @@ -1211,7 +1101,7 @@ This rule is from a remote directory.
}

// Run the tool with the manifest file
output := runTool(t, "-C", mainDir, "-m", "file://"+manifestFile, "/test-task")
output := runTool(t, "-C", mainDir, "-m", "file://"+manifestFile, "test-task")

// Check that the main rule is included
if !strings.Contains(output, "# Main Rule") {
Expand Down
Loading