From 8935c6df68946bd81f3d39e40b3bc2f399173777 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:06:11 +0000 Subject: [PATCH 1/4] Initial plan From 6aff58d42b15e6e52f9ecf6946434c24b24b658e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:16:24 +0000 Subject: [PATCH 2/4] Implement basic coding-context CLI with import, export, bootstrap, and prompt commands Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --- internal/agents/agents.go | 407 +++++++++++++++++++++++++++++++++ internal/commands/bootstrap.go | 46 ++++ internal/commands/export.go | 156 +++++++++++++ internal/commands/import.go | 98 ++++++++ internal/commands/prompt.go | 39 ++++ main.go | 162 +++++-------- 6 files changed, 805 insertions(+), 103 deletions(-) create mode 100644 internal/agents/agents.go create mode 100644 internal/commands/bootstrap.go create mode 100644 internal/commands/export.go create mode 100644 internal/commands/import.go create mode 100644 internal/commands/prompt.go diff --git a/internal/agents/agents.go b/internal/agents/agents.go new file mode 100644 index 0000000..895f599 --- /dev/null +++ b/internal/agents/agents.go @@ -0,0 +1,407 @@ +package agents + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +// Agent represents a coding agent type +type Agent string + +const ( + Claude Agent = "Claude" + Gemini Agent = "Gemini" + Codex Agent = "Codex" + Cursor Agent = "Cursor" + Augment Agent = "Augment" + GitHubCopilot Agent = "GitHubCopilot" + Windsurf Agent = "Windsurf" + Goose Agent = "Goose" + Default Agent = "" // Default agent using .prompts directories +) + +// RuleLevel represents the priority level of rules +type RuleLevel int + +const ( + ProjectLevel RuleLevel = 0 // Most important + AncestorLevel RuleLevel = 1 // Next most important + UserLevel RuleLevel = 2 // User-specific + SystemLevel RuleLevel = 3 // System-wide +) + +// RulePathFunc returns the list of rule file paths for a given agent at a specific level +type RulePathFunc func(level int) ([]string, error) + +// AgentRulePath maps agent names to their rule path functions +var AgentRulePath = map[Agent]RulePathFunc{ + Default: getDefaultRulePaths, + Claude: getClaudeRulePaths, + Gemini: getGeminiRulePaths, + Codex: getCodexRulePaths, + Cursor: getCursorRulePaths, + Augment: getAugmentRulePaths, + GitHubCopilot: getGitHubCopilotRulePaths, + Windsurf: getWindsurfRulePaths, + Goose: getGooseRulePaths, +} + +// getDefaultRulePaths returns paths for the default agent (.prompts) +func getDefaultRulePaths(level int) ([]string, error) { + var paths []string + + switch RuleLevel(level) { + case ProjectLevel: + // .prompts directory in PWD + if entries, err := listMarkdownFiles(".prompts"); err == nil { + paths = append(paths, entries...) + } + case UserLevel: + // ~/.prompts/rules directory + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + rulesDir := filepath.Join(home, ".prompts", "rules") + if entries, err := listMarkdownFiles(rulesDir); err == nil { + paths = append(paths, entries...) + } + } + + return paths, nil +} + +// getClaudeRulePaths returns paths for Claude agent +func getClaudeRulePaths(level int) ([]string, error) { + var paths []string + + switch RuleLevel(level) { + case ProjectLevel: + // ./CLAUDE.local.md + if exists("CLAUDE.local.md") { + paths = append(paths, "CLAUDE.local.md") + } + case AncestorLevel: + // ./CLAUDE.md in ancestor directories + ancestorPaths := findAncestorFiles("CLAUDE.md") + paths = append(paths, ancestorPaths...) + case UserLevel: + // ~/.claude/CLAUDE.md + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + globalPath := filepath.Join(home, ".claude", "CLAUDE.md") + if exists(globalPath) { + paths = append(paths, globalPath) + } + } + + return paths, nil +} + +// getGeminiRulePaths returns paths for Gemini agent +func getGeminiRulePaths(level int) ([]string, error) { + var paths []string + + switch RuleLevel(level) { + case ProjectLevel: + // ./.gemini/styleguide.md + styleguide := filepath.Join(".gemini", "styleguide.md") + if exists(styleguide) { + paths = append(paths, styleguide) + } + case AncestorLevel: + // ./GEMINI.md in ancestor directories + ancestorPaths := findAncestorFiles("GEMINI.md") + paths = append(paths, ancestorPaths...) + case UserLevel: + // ~/.gemini/GEMINI.md + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + globalPath := filepath.Join(home, ".gemini", "GEMINI.md") + if exists(globalPath) { + paths = append(paths, globalPath) + } + } + + return paths, nil +} + +// getCodexRulePaths returns paths for Codex agent +func getCodexRulePaths(level int) ([]string, error) { + var paths []string + + switch RuleLevel(level) { + case AncestorLevel: + // AGENTS.md in ancestor directories and CWD + ancestorPaths := findAncestorFiles("AGENTS.md") + paths = append(paths, ancestorPaths...) + case UserLevel: + // ~/.codex/AGENTS.md + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + globalPath := filepath.Join(home, ".codex", "AGENTS.md") + if exists(globalPath) { + paths = append(paths, globalPath) + } + } + + return paths, nil +} + +// getCursorRulePaths returns paths for Cursor agent +func getCursorRulePaths(level int) ([]string, error) { + var paths []string + + switch RuleLevel(level) { + case ProjectLevel: + // ./.cursor/rules/ directory (*.mdc and *.md files) + rulesDir := filepath.Join(".cursor", "rules") + if entries, err := listMarkdownFiles(rulesDir); err == nil { + paths = append(paths, entries...) + } + // Also check for .mdc files + if entries, err := listMDCFiles(rulesDir); err == nil { + paths = append(paths, entries...) + } + // AGENTS.md at project root + if exists("AGENTS.md") { + paths = append(paths, "AGENTS.md") + } + } + + return paths, nil +} + +// getAugmentRulePaths returns paths for Augment agent +func getAugmentRulePaths(level int) ([]string, error) { + var paths []string + + switch RuleLevel(level) { + case ProjectLevel: + // /.augment/rules/ + rulesDir := filepath.Join(".augment", "rules") + if entries, err := listMarkdownFiles(rulesDir); err == nil { + paths = append(paths, entries...) + } + // Legacy: /.augment/guidelines.md + guidelines := filepath.Join(".augment", "guidelines.md") + if exists(guidelines) { + paths = append(paths, guidelines) + } + case AncestorLevel: + // Compatibility: CLAUDE.md and AGENTS.md in ancestor directories + claudePaths := findAncestorFiles("CLAUDE.md") + paths = append(paths, claudePaths...) + agentsPaths := findAncestorFiles("AGENTS.md") + paths = append(paths, agentsPaths...) + } + + return paths, nil +} + +// getGitHubCopilotRulePaths returns paths for GitHub Copilot agent +func getGitHubCopilotRulePaths(level int) ([]string, error) { + var paths []string + + switch RuleLevel(level) { + case ProjectLevel: + // .github/agents directory + agentsDir := filepath.Join(".github", "agents") + if entries, err := listMarkdownFiles(agentsDir); err == nil { + paths = append(paths, entries...) + } + case AncestorLevel: + // ./.github/copilot-instructions.md + instructionsPath := filepath.Join(".github", "copilot-instructions.md") + if exists(instructionsPath) { + paths = append(paths, instructionsPath) + } + // ./AGENTS.md (nearest in directory tree) + ancestorPaths := findAncestorFiles("AGENTS.md") + if len(ancestorPaths) > 0 { + // Only take the nearest one + paths = append(paths, ancestorPaths[0]) + } + } + + return paths, nil +} + +// getWindsurfRulePaths returns paths for Windsurf agent +func getWindsurfRulePaths(level int) ([]string, error) { + var paths []string + + switch RuleLevel(level) { + case ProjectLevel, AncestorLevel: + // ./.windsurf/rules/ (searched from workspace up to Git root) + rulesDir := filepath.Join(".windsurf", "rules") + if entries, err := listMarkdownFiles(rulesDir); err == nil { + paths = append(paths, entries...) + } + // Also check ancestor directories for .windsurf/rules + if RuleLevel(level) == AncestorLevel { + ancestors := getAncestorDirs() + for _, ancestor := range ancestors { + ancestorRulesDir := filepath.Join(ancestor, ".windsurf", "rules") + if entries, err := listMarkdownFiles(ancestorRulesDir); err == nil { + paths = append(paths, entries...) + } + } + } + } + + return paths, nil +} + +// getGooseRulePaths returns paths for Goose agent +func getGooseRulePaths(level int) ([]string, error) { + var paths []string + + switch RuleLevel(level) { + case AncestorLevel: + // Relies on standard AGENTS.md + ancestorPaths := findAncestorFiles("AGENTS.md") + paths = append(paths, ancestorPaths...) + } + + return paths, nil +} + +// Helper functions + +// exists checks if a file exists +func exists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +// listMarkdownFiles lists all .md files in a directory +func listMarkdownFiles(dir string) ([]string, error) { + var files []string + + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".md") { + files = append(files, filepath.Join(dir, entry.Name())) + } + } + + return files, nil +} + +// listMDCFiles lists all .mdc files in a directory +func listMDCFiles(dir string) ([]string, error) { + var files []string + + entries, err := os.ReadDir(dir) + if err != nil { + return nil, err + } + + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".mdc") { + files = append(files, filepath.Join(dir, entry.Name())) + } + } + + return files, nil +} + +// findAncestorFiles finds all instances of a file in ancestor directories +func findAncestorFiles(filename string) []string { + var paths []string + + cwd, err := os.Getwd() + if err != nil { + return paths + } + + dir := cwd + for { + filePath := filepath.Join(dir, filename) + if exists(filePath) { + paths = append(paths, filePath) + } + + parent := filepath.Dir(dir) + if parent == dir { + // Reached root + break + } + dir = parent + } + + return paths +} + +// getAncestorDirs returns all ancestor directories up to root +func getAncestorDirs() []string { + var dirs []string + + cwd, err := os.Getwd() + if err != nil { + return dirs + } + + dir := filepath.Dir(cwd) + for { + dirs = append(dirs, dir) + + parent := filepath.Dir(dir) + if parent == dir { + // Reached root + break + } + dir = parent + } + + return dirs +} + +// GetAllAgents returns a list of all supported agents +func GetAllAgents() []Agent { + return []Agent{ + Default, + Claude, + Gemini, + Codex, + Cursor, + Augment, + GitHubCopilot, + Windsurf, + Goose, + } +} + +// GetRulePaths returns all rule paths for a given agent +func GetRulePaths(agent Agent) ([]string, error) { + pathFunc, ok := AgentRulePath[agent] + if !ok { + return nil, fmt.Errorf("unknown agent: %s", agent) + } + + var allPaths []string + + // Collect paths from all levels + for level := 0; level <= int(SystemLevel); level++ { + paths, err := pathFunc(level) + if err != nil { + return nil, err + } + allPaths = append(allPaths, paths...) + } + + return allPaths, nil +} diff --git a/internal/commands/bootstrap.go b/internal/commands/bootstrap.go new file mode 100644 index 0000000..7972796 --- /dev/null +++ b/internal/commands/bootstrap.go @@ -0,0 +1,46 @@ +package commands + +import ( + "fmt" + "os" + "path/filepath" +) + +// Bootstrap creates the necessary directory structure and files +func Bootstrap() error { + fmt.Println("Bootstrapping coding-context...") + + // Create AGENTS.md if it doesn't exist + agentsFile := "AGENTS.md" + if _, err := os.Stat(agentsFile); os.IsNotExist(err) { + fmt.Printf("Creating %s\n", agentsFile) + defaultContent := []byte("# Coding Agent Rules\n\nThis file contains rules and guidelines for coding agents.\n") + if err := os.WriteFile(agentsFile, defaultContent, 0644); err != nil { + return fmt.Errorf("failed to create AGENTS.md: %w", err) + } + } else { + fmt.Printf("%s already exists\n", agentsFile) + } + + // Create .prompts directory + promptsDir := ".prompts" + if err := os.MkdirAll(promptsDir, 0755); err != nil { + return fmt.Errorf("failed to create %s: %w", promptsDir, err) + } + fmt.Printf("Created %s directory\n", promptsDir) + + // Create user-level directory structure + home, err := os.UserHomeDir() + if err != nil { + return fmt.Errorf("failed to get home directory: %w", err) + } + + userPromptsDir := filepath.Join(home, ".prompts", "rules") + if err := os.MkdirAll(userPromptsDir, 0755); err != nil { + return fmt.Errorf("failed to create %s: %w", userPromptsDir, err) + } + fmt.Printf("Created %s directory\n", userPromptsDir) + + fmt.Println("\nBootstrap complete!") + return nil +} diff --git a/internal/commands/export.go b/internal/commands/export.go new file mode 100644 index 0000000..f0b483b --- /dev/null +++ b/internal/commands/export.go @@ -0,0 +1,156 @@ +package commands + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/kitproj/kit/internal/agents" +) + +// Export exports rules from the normalized format to a specific agent +func Export(agentName string) error { + // Parse agent name + var targetAgent agents.Agent + switch strings.ToLower(agentName) { + case "claude": + targetAgent = agents.Claude + case "gemini": + targetAgent = agents.Gemini + case "codex": + targetAgent = agents.Codex + case "cursor": + targetAgent = agents.Cursor + case "augment": + targetAgent = agents.Augment + case "githubcopilot", "copilot": + targetAgent = agents.GitHubCopilot + case "windsurf": + targetAgent = agents.Windsurf + case "goose": + targetAgent = agents.Goose + default: + return fmt.Errorf("unknown agent: %s", agentName) + } + + fmt.Printf("Exporting rules to %s...\n", targetAgent) + + // Read the normalized AGENTS.md file + agentsFile := "AGENTS.md" + content, err := os.ReadFile(agentsFile) + if err != nil { + return fmt.Errorf("failed to read AGENTS.md: %w", err) + } + + // Export to the target agent's format + if err := exportToAgent(targetAgent, content); err != nil { + return fmt.Errorf("failed to export to %s: %w", targetAgent, err) + } + + fmt.Printf("Export to %s complete!\n", targetAgent) + return nil +} + +// exportToAgent exports content to a specific agent's format +func exportToAgent(agent agents.Agent, content []byte) error { + switch agent { + case agents.Claude: + return exportToClaude(content) + case agents.Gemini: + return exportToGemini(content) + case agents.Codex: + return exportToCodex(content) + case agents.Cursor: + return exportToCursor(content) + case agents.Augment: + return exportToAugment(content) + case agents.GitHubCopilot: + return exportToGitHubCopilot(content) + case agents.Windsurf: + return exportToWindsurf(content) + case agents.Goose: + return exportToGoose(content) + default: + return fmt.Errorf("export not implemented for agent: %s", agent) + } +} + +func exportToClaude(content []byte) error { + // Export to ./CLAUDE.md + path := "CLAUDE.md" + fmt.Printf(" Writing to %s\n", path) + return os.WriteFile(path, content, 0644) +} + +func exportToGemini(content []byte) error { + // Export to ./GEMINI.md + path := "GEMINI.md" + fmt.Printf(" Writing to %s\n", path) + return os.WriteFile(path, content, 0644) +} + +func exportToCodex(content []byte) error { + // Export to ./AGENTS.md (same as normalized) + path := "AGENTS.md" + fmt.Printf(" Using existing %s\n", path) + return nil +} + +func exportToCursor(content []byte) error { + // Export to ./AGENTS.md (compatibility mode) + path := "AGENTS.md" + fmt.Printf(" Using existing %s\n", path) + return nil +} + +func exportToAugment(content []byte) error { + // Export to .augment/rules/AGENTS.md + dir := filepath.Join(".augment", "rules") + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + path := filepath.Join(dir, "AGENTS.md") + fmt.Printf(" Writing to %s\n", path) + return os.WriteFile(path, content, 0644) +} + +func exportToGitHubCopilot(content []byte) error { + // Export to ./AGENTS.md (compatibility) and .github/copilot-instructions.md + paths := []string{ + "AGENTS.md", + filepath.Join(".github", "copilot-instructions.md"), + } + + for _, path := range paths { + dir := filepath.Dir(path) + if dir != "." { + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + } + fmt.Printf(" Writing to %s\n", path) + if err := os.WriteFile(path, content, 0644); err != nil { + return err + } + } + return nil +} + +func exportToWindsurf(content []byte) error { + // Export to .windsurf/rules/AGENTS.md + dir := filepath.Join(".windsurf", "rules") + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + path := filepath.Join(dir, "AGENTS.md") + fmt.Printf(" Writing to %s\n", path) + return os.WriteFile(path, content, 0644) +} + +func exportToGoose(content []byte) error { + // Export to ./AGENTS.md (same as normalized) + path := "AGENTS.md" + fmt.Printf(" Using existing %s\n", path) + return nil +} diff --git a/internal/commands/import.go b/internal/commands/import.go new file mode 100644 index 0000000..3767d85 --- /dev/null +++ b/internal/commands/import.go @@ -0,0 +1,98 @@ +package commands + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/kitproj/kit/internal/agents" +) + +// Import imports rules from all agents into the normalized format +func Import() error { + fmt.Println("Importing rules from all agents...") + + // Open AGENTS.md for appending + agentsFile := "AGENTS.md" + + // Get absolute path to AGENTS.md to avoid importing it + agentsFilePath, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current directory: %w", err) + } + agentsFilePath = agentsFilePath + "/AGENTS.md" + + f, err := os.OpenFile(agentsFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return fmt.Errorf("failed to open AGENTS.md: %w", err) + } + defer f.Close() + + // Process each agent + for _, agent := range agents.GetAllAgents() { + if agent == agents.Default { + // Skip the default agent (normalized format) + continue + } + + fmt.Printf("\nProcessing agent: %s\n", agent) + + paths, err := agents.GetRulePaths(agent) + if err != nil { + fmt.Printf(" Warning: failed to get paths for %s: %v\n", agent, err) + continue + } + + if len(paths) == 0 { + fmt.Printf(" No rules found for %s\n", agent) + continue + } + + // Process each rule file, skip AGENTS.md to avoid recursion + for _, path := range paths { + absPath := path + if !filepath.IsAbs(path) { + cwd, _ := os.Getwd() + absPath = filepath.Join(cwd, path) + } + + // Skip AGENTS.md to avoid importing itself + if absPath == agentsFilePath { + continue + } + + if err := importFile(f, agent, path); err != nil { + fmt.Printf(" Warning: failed to import %s: %v\n", path, err) + } + } + } + + fmt.Println("\nImport complete!") + return nil +} + +// importFile imports a single file into AGENTS.md +func importFile(w io.Writer, agent agents.Agent, path string) error { + fmt.Printf(" Importing: %s\n", path) + + // Read the file content + content, err := os.ReadFile(path) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + + // Write to AGENTS.md with a header + _, err = fmt.Fprintf(w, "\n\n", agent, path) + if err != nil { + return err + } + + _, err = w.Write(content) + if err != nil { + return err + } + + _, err = w.Write([]byte("\n")) + return err +} diff --git a/internal/commands/prompt.go b/internal/commands/prompt.go new file mode 100644 index 0000000..fccd746 --- /dev/null +++ b/internal/commands/prompt.go @@ -0,0 +1,39 @@ +package commands + +import ( + "fmt" + "os" + + "github.com/kitproj/kit/internal/agents" +) + +// Prompt prints the aggregated prompt from all rule files +func Prompt() error { + fmt.Println("# Aggregated Prompt from All Rules\n") + + // Get paths from the default agent (normalized format) + paths, err := agents.GetRulePaths(agents.Default) + if err != nil { + return fmt.Errorf("failed to get rule paths: %w", err) + } + + if len(paths) == 0 { + fmt.Println("No rule files found.") + return nil + } + + // Print each file's content + for _, path := range paths { + content, err := os.ReadFile(path) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to read %s: %v\n", path, err) + continue + } + + fmt.Printf("\n", path) + fmt.Println(string(content)) + fmt.Println() + } + + return nil +} diff --git a/main.go b/main.go index 71eab3a..a9b6adc 100644 --- a/main.go +++ b/main.go @@ -1,125 +1,81 @@ package main import ( - "context" - _ "embed" - "flag" "fmt" - "log" "os" - "os/signal" "runtime/debug" - "strings" - "syscall" - "github.com/kitproj/kit/internal" - "github.com/kitproj/kit/internal/types" - "sigs.k8s.io/yaml" + "github.com/kitproj/kit/internal/commands" ) -func init() { - log.SetOutput(os.Stdout) - log.SetFlags(0) -} - func main() { - help := false - printVersion := false - workingDir := "." - configFile := "" - tasksToSkip := "" - port := -1 // -1 means unspecified, 0 means disabled, >0 means specified - openBrowser := false - rewrite := false - - flag.BoolVar(&help, "h", false, "print help and exit") - flag.BoolVar(&printVersion, "v", false, "print version and exit") - flag.StringVar(&workingDir, "C", ".", "working directory (default current directory)") - flag.StringVar(&configFile, "f", "tasks.yaml", "config file (default tasks.yaml)") - flag.StringVar(&tasksToSkip, "s", "", "tasks to skip (comma separated)") - flag.IntVar(&port, "p", port, "port to start UI on (default 3000, zero disables)") - flag.BoolVar(&openBrowser, "b", false, "open the UI in the browser (default false)") - flag.BoolVar(&rewrite, "w", false, "rewrite the config file") - flag.Parse() - taskNames := flag.Args() - - if help { - flag.Usage() - os.Exit(0) - } - - if printVersion { - info, _ := debug.ReadBuildInfo() - fmt.Printf("%v\n", info.Main.Version) - os.Exit(0) - } - - if err := os.Chdir(workingDir); err != nil { - _, _ = fmt.Fprintf(os.Stderr, "failed to change to directory %s: %v\n", workingDir, err) + if len(os.Args) < 2 { + printUsage() os.Exit(1) } - err := func() error { - - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) - defer cancel() - - wf := &types.Workflow{} + command := os.Args[1] - in, err := os.ReadFile(configFile) - if err != nil { - return fmt.Errorf("failed to read %s: %w", configFile, err) + switch command { + case "import": + if err := commands.Import(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) } - if err = yaml.UnmarshalStrict(in, wf); err != nil { - return fmt.Errorf("failed to parse %s: %w", configFile, err) + case "export": + if len(os.Args) < 3 { + fmt.Fprintf(os.Stderr, "Error: export command requires an agent name\n") + fmt.Fprintf(os.Stderr, "Usage: coding-context export \n") + fmt.Fprintf(os.Stderr, "Available agents: Claude, Gemini, Codex, Cursor, Augment, GitHubCopilot, Windsurf, Goose\n") + os.Exit(1) } - - if rewrite { - out, err := yaml.Marshal(wf) - if err != nil { - return fmt.Errorf("failed to marshal %s: %w", configFile, err) - } - return os.WriteFile(configFile, out, 0644) - } - - // if wf.Port is specified, use that, unless the user has specified a port on the command line - if port == -1 { - if wf.Port != nil { - port = int(*wf.Port) - } else { - port = 3000 // default port - } + agentName := os.Args[2] + if err := commands.Export(agentName); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) } - - // split the tasks on comma, but don't end up with a single entry of "" - split := strings.Split(tasksToSkip, ",") - if len(split) == 1 && split[0] == "" { - split = []string{} + case "bootstrap": + if err := commands.Bootstrap(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) } - - if len(taskNames) == 0 { - for taskName, task := range wf.Tasks { - if task.Default { - taskNames = []string{taskName} - break - } - } + case "prompt": + if err := commands.Prompt(); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) } - - return internal.RunSubgraph( - ctx, - cancel, - port, - openBrowser, - log.Default(), - wf, - taskNames, - split, - ) - }() - - if err != nil { - _, _ = fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + case "-v", "--version": + info, _ := debug.ReadBuildInfo() + fmt.Printf("%v\n", info.Main.Version) + case "-h", "--help", "help": + printUsage() + default: + fmt.Fprintf(os.Stderr, "Error: unknown command: %s\n\n", command) + printUsage() os.Exit(1) } } + +func printUsage() { + fmt.Println("coding-context - Manage coding agent rules") + fmt.Println() + fmt.Println("Usage:") + fmt.Println(" coding-context [arguments]") + fmt.Println() + fmt.Println("Commands:") + fmt.Println(" import Import rules from all agents into AGENTS.md") + fmt.Println(" export Export rules to a specific agent format") + fmt.Println(" bootstrap Create initial directory structure and files") + fmt.Println(" prompt Print the aggregated prompt from all rules") + fmt.Println(" -v, --version Print version information") + fmt.Println(" -h, --help Print this help message") + fmt.Println() + fmt.Println("Supported agents:") + fmt.Println(" Claude, Gemini, Codex, Cursor, Augment, GitHubCopilot, Windsurf, Goose") + fmt.Println() + fmt.Println("Examples:") + fmt.Println(" coding-context import") + fmt.Println(" coding-context export Gemini") + fmt.Println(" coding-context bootstrap") + fmt.Println(" coding-context prompt") +} From dbdd740a608d083c1e0a78a29bfb78aa615eaa06 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:18:57 +0000 Subject: [PATCH 3/4] Add tests and update documentation for coding-context CLI Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --- README.md | 235 +++++++++++++---------------- internal/agents/agents_test.go | 165 ++++++++++++++++++++ internal/commands/commands_test.go | 169 +++++++++++++++++++++ internal/commands/prompt.go | 3 +- 4 files changed, 438 insertions(+), 134 deletions(-) create mode 100644 internal/agents/agents_test.go create mode 100644 internal/commands/commands_test.go diff --git a/README.md b/README.md index b7cdd63..54ac7d1 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,26 @@ -# Kit - Unified Workflow Engine for Software Development +# coding-context - Manage Coding Agent Rules -[![CodeQL](https://github.com/kitproj/kit/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/kitproj/kit/actions/workflows/codeql-analysis.yml) -[![Go](https://github.com/kitproj/kit/actions/workflows/go.yml/badge.svg)](https://github.com/kitproj/kit/actions/workflows/go.yml) +A unified CLI tool for managing coding rules across different AI coding agents (Claude, Gemini, Cursor, GitHub Copilot, and more). -## What is Kit? +## What is coding-context? -Kit is a powerful workflow engine that simplifies complex software development environments by combining multiple tools into a single binary: +coding-context simplifies the management of coding rules and guidelines across multiple AI coding agents. Instead of maintaining separate rule files for each agent, you can: -- **Task execution** (like Makefile or Taskfile) -- **Service orchestration** (like Foreman) -- **Container management** (like Docker Compose) -- **Kubernetes resource management** (like Tilt, Skaffold) -- **Local development focus** (like Garden) +- **Import** rules from all agents into a normalized format +- **Export** rules to specific agent formats +- **Bootstrap** the necessary directory structure +- **View** aggregated prompts from all your rules -With Kit, you can define and manage complex workflows in a single `tasks.yaml` file, making it ideal for projects that require running multiple components simultaneously. +## Supported Agents -![Kit UI Screenshot](img.png) - -## Key Features - -- **Single binary** - Easy to install and use -- **Dependency management** - Define task dependencies in a DAG -- **Multiple task types** - Run commands, containers, Kubernetes resources -- **Auto-restart** - Automatically restart services on failure -- **File watching** - Re-run tasks when files change -- **Port forwarding** - Forward ports from services to host -- **Web UI** - Visualize your workflow and monitor task status with real-time metrics +- **Claude** - Hierarchical concatenation with CLAUDE.md files +- **Gemini** - Strategic layer with GEMINI.md files +- **Codex** - Uses AGENTS.md in ancestor directories +- **Cursor** - Declarative context injection with .cursor/rules/ +- **Augment** - Structured rules in .augment/rules/ +- **GitHub Copilot** - Uses .github/copilot-instructions.md +- **Windsurf** - Rules in .windsurf/rules/ +- **Goose** - Standard AGENTS.md compatibility ## Quick Start @@ -35,8 +30,8 @@ Download the standalone binary from the [releases page](https://github.com/kitpr ```bash # For Linux -sudo curl --fail --location --output /usr/local/bin/kit https://github.com/kitproj/kit/releases/download/v0.1.105/kit_v0.1.105_linux_386 -sudo chmod +x /usr/local/bin/kit +sudo curl --fail --location --output /usr/local/bin/coding-context https://github.com/kitproj/kit/releases/download/v0.1.105/kit_v0.1.105_linux_386 +sudo chmod +x /usr/local/bin/coding-context # For Go users go install github.com/kitproj/kit@v0.1.105 @@ -44,154 +39,128 @@ go install github.com/kitproj/kit@v0.1.105 ### Basic Usage -1. Create a `tasks.yaml` file in your project: - -```yaml -tasks: - build: - command: go build . - watch: . # Auto-rebuild when files change - run: - dependencies: [build] - command: ./myapp - ports: [8080] # Define as a service that listens on port 8080 +1. **Bootstrap** - Create initial directory structure: + +```bash +coding-context bootstrap ``` -2. Start your workflow: +2. **Import** - Gather rules from all agents: ```bash -kit run # Run the 'run' task and its dependencies +coding-context import ``` -## Core Concepts +3. **Export** - Distribute rules to a specific agent: -### Jobs vs Services - -- **Jobs**: Run once and exit (default) -- **Services**: Run indefinitely and listen on ports +```bash +coding-context export Gemini +``` -```yaml -# Job example -build: - command: go build . +4. **View Prompt** - See aggregated rules: -# Service example -api: - command: ./api-server - ports: [8080] +```bash +coding-context prompt ``` -Services automatically restart on failure. Configure restart behavior with `restartPolicy` (Always, Never, OnFailure). - -### Task Types +## Commands -#### Host Tasks +### import -Run commands on your local machine: +Import rules from all supported agents into the normalized AGENTS.md format: -```yaml -build: - command: go build . +```bash +coding-context import ``` -#### Shell Tasks +This command: +- Scans for rules from all supported agents +- Appends them to AGENTS.md with source annotations +- Converts .mdc files to .md format +- Skips duplicate imports of AGENTS.md -Run shell scripts: +### export -```yaml -setup: - sh: | - set -eux - echo "Setting up environment..." - mkdir -p ./data -``` +Export rules from AGENTS.md to a specific agent's format: -#### Container Tasks +```bash +coding-context export +``` -Run Docker containers: +Available agents: Claude, Gemini, Codex, Cursor, Augment, GitHubCopilot, Windsurf, Goose -```yaml -database: - image: postgres:14 - ports: [5432:5432] - env: - - POSTGRES_PASSWORD=password +Examples: +```bash +coding-context export Gemini # Creates GEMINI.md +coding-context export Claude # Creates CLAUDE.md +coding-context export Windsurf # Creates .windsurf/rules/AGENTS.md ``` -Kit can also build and run containers from a Dockerfile: +### bootstrap + +Create the initial directory structure and files: -```yaml -api: - image: ./src/api # Directory with Dockerfile - ports: [8080] +```bash +coding-context bootstrap ``` -#### Kubernetes Tasks +This creates: +- `AGENTS.md` - Main rules file +- `.prompts/` - Project-level rules directory +- `~/.prompts/rules/` - User-level rules directory -Deploy and manage Kubernetes resources: +### prompt -```yaml -deploy: - namespace: default - manifests: - - k8s/ - - service.yaml - ports: [80:8080] # Forward cluster port 80 to local port 8080 +Print the aggregated prompt from all rule files: + +```bash +coding-context prompt ``` -### Advanced Features +This displays the content of all rule files in the normalized format. -#### Task Dependencies +## Rule Hierarchy -```yaml -test: - dependencies: [build, database] - command: go test ./... -``` +Rules are organized by priority level: -#### Environment Variables +1. **Project Rules (Level 0)** - Most important + - `.prompts/` directory + - Project-specific files like `.github/agents/` -```yaml -server: - command: ./server - env: - - PORT=8080 - - DEBUG=true - envfile: .env # Load from file -``` +2. **Ancestor Rules (Level 1)** - Next priority + - Files in parent directories (e.g., AGENTS.md, CLAUDE.md) -#### File Watching +3. **User Rules (Level 2)** - User-specific + - `~/.prompts/rules/` + - `~/.claude/CLAUDE.md` + - `~/.gemini/GEMINI.md` -```yaml -build: - command: go build . - watch: src/ # Rebuild when files in src/ change -``` +4. **System Rules (Level 3)** - System-wide + - `/usr/local/prompts-rules` -#### Task Grouping - -Organize tasks visually in the UI: - -```yaml -tasks: - api: - command: ./api - ports: [8080] - group: backend - db: - image: postgres - ports: [5432] - group: backend - ui: - command: npm start - ports: [3000] - group: frontend -``` +## Agent-Specific Formats + +### Claude +- Global: `~/.claude/CLAUDE.md` +- Ancestor: `./CLAUDE.md` (checked into Git) +- Local: `./CLAUDE.local.md` (highest precedence, typically .gitignore'd) + +### Gemini +- Global: `~/.gemini/GEMINI.md` +- Ancestor: `./GEMINI.md` +- Project: `./.gemini/styleguide.md` + +### Cursor +- Project: `./.cursor/rules/*.md` and `./.cursor/rules/*.mdc` +- Compatibility: `./AGENTS.md` -## Documentation +### GitHub Copilot +- Repository: `./.github/copilot-instructions.md` +- Agent Config: `.github/agents/` +- Compatibility: `./AGENTS.md` -- [Examples](docs/examples) - Practical examples (MySQL, Kafka, etc.) -- [Reference](docs/reference) - Detailed configuration options +### Windsurf +- Project/Ancestor: `./.windsurf/rules/*.md` ## Contributing diff --git a/internal/agents/agents_test.go b/internal/agents/agents_test.go new file mode 100644 index 0000000..20d28e4 --- /dev/null +++ b/internal/agents/agents_test.go @@ -0,0 +1,165 @@ +package agents + +import ( + "os" + "path/filepath" + "testing" +) + +func TestGetAllAgents(t *testing.T) { + agents := GetAllAgents() + if len(agents) != 9 { + t.Errorf("Expected 9 agents, got %d", len(agents)) + } + + // Verify expected agents are present + expectedAgents := map[Agent]bool{ + Default: true, + Claude: true, + Gemini: true, + Codex: true, + Cursor: true, + Augment: true, + GitHubCopilot: true, + Windsurf: true, + Goose: true, + } + + for _, agent := range agents { + if !expectedAgents[agent] { + t.Errorf("Unexpected agent: %s", agent) + } + } +} + +func TestGetRulePaths(t *testing.T) { + // Create a temporary directory for testing + tmpDir, err := os.MkdirTemp("", "agents-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Change to the temp directory + oldWd, _ := os.Getwd() + defer os.Chdir(oldWd) + os.Chdir(tmpDir) + + // Test Default agent with .prompts directory + os.MkdirAll(".prompts", 0755) + os.WriteFile(filepath.Join(".prompts", "test.md"), []byte("test"), 0644) + + paths, err := GetRulePaths(Default) + if err != nil { + t.Errorf("GetRulePaths(Default) failed: %v", err) + } + if len(paths) == 0 { + t.Error("Expected at least one path for Default agent") + } + + // Test Claude agent + os.WriteFile("CLAUDE.md", []byte("test"), 0644) + paths, err = GetRulePaths(Claude) + if err != nil { + t.Errorf("GetRulePaths(Claude) failed: %v", err) + } + if len(paths) == 0 { + t.Error("Expected at least one path for Claude agent") + } + + // Test unknown agent + _, err = GetRulePaths("UnknownAgent") + if err == nil { + t.Error("Expected error for unknown agent") + } +} + +func TestExists(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "exists-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + testFile := filepath.Join(tmpDir, "test.txt") + + if exists(testFile) { + t.Error("exists() returned true for non-existent file") + } + + os.WriteFile(testFile, []byte("test"), 0644) + + if !exists(testFile) { + t.Error("exists() returned false for existing file") + } +} + +func TestListMarkdownFiles(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "md-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create some test files + os.WriteFile(filepath.Join(tmpDir, "file1.md"), []byte("test"), 0644) + os.WriteFile(filepath.Join(tmpDir, "file2.md"), []byte("test"), 0644) + os.WriteFile(filepath.Join(tmpDir, "file3.txt"), []byte("test"), 0644) + + files, err := listMarkdownFiles(tmpDir) + if err != nil { + t.Fatalf("listMarkdownFiles failed: %v", err) + } + + if len(files) != 2 { + t.Errorf("Expected 2 markdown files, got %d", len(files)) + } +} + +func TestListMDCFiles(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "mdc-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create some test files + os.WriteFile(filepath.Join(tmpDir, "file1.mdc"), []byte("test"), 0644) + os.WriteFile(filepath.Join(tmpDir, "file2.mdc"), []byte("test"), 0644) + os.WriteFile(filepath.Join(tmpDir, "file3.md"), []byte("test"), 0644) + + files, err := listMDCFiles(tmpDir) + if err != nil { + t.Fatalf("listMDCFiles failed: %v", err) + } + + if len(files) != 2 { + t.Errorf("Expected 2 mdc files, got %d", len(files)) + } +} + +func TestFindAncestorFiles(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "ancestor-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Create nested directory structure + subdir := filepath.Join(tmpDir, "a", "b", "c") + os.MkdirAll(subdir, 0755) + + // Create test files at different levels + os.WriteFile(filepath.Join(tmpDir, "TEST.md"), []byte("test"), 0644) + os.WriteFile(filepath.Join(tmpDir, "a", "TEST.md"), []byte("test"), 0644) + + // Change to subdirectory + oldWd, _ := os.Getwd() + defer os.Chdir(oldWd) + os.Chdir(subdir) + + paths := findAncestorFiles("TEST.md") + if len(paths) < 2 { + t.Errorf("Expected at least 2 ancestor files, got %d", len(paths)) + } +} diff --git a/internal/commands/commands_test.go b/internal/commands/commands_test.go new file mode 100644 index 0000000..661b99a --- /dev/null +++ b/internal/commands/commands_test.go @@ -0,0 +1,169 @@ +package commands + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestBootstrap(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "bootstrap-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Change to temp directory + oldWd, _ := os.Getwd() + defer os.Chdir(oldWd) + os.Chdir(tmpDir) + + // Run bootstrap + err = Bootstrap() + if err != nil { + t.Errorf("Bootstrap failed: %v", err) + } + + // Check if AGENTS.md was created + if _, err := os.Stat("AGENTS.md"); os.IsNotExist(err) { + t.Error("AGENTS.md was not created") + } + + // Check if .prompts directory was created + if _, err := os.Stat(".prompts"); os.IsNotExist(err) { + t.Error(".prompts directory was not created") + } +} + +func TestImport(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "import-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Change to temp directory + oldWd, _ := os.Getwd() + defer os.Chdir(oldWd) + os.Chdir(tmpDir) + + // Create test files + os.WriteFile("CLAUDE.md", []byte("# Claude Test"), 0644) + os.MkdirAll(".cursor/rules", 0755) + os.WriteFile(".cursor/rules/test.mdc", []byte("# Cursor Test"), 0644) + + // Create AGENTS.md + os.WriteFile("AGENTS.md", []byte("# Initial Content\n"), 0644) + + // Run import + err = Import() + if err != nil { + t.Errorf("Import failed: %v", err) + } + + // Check if content was appended to AGENTS.md + content, err := os.ReadFile("AGENTS.md") + if err != nil { + t.Fatalf("Failed to read AGENTS.md: %v", err) + } + + contentStr := string(content) + if !strings.Contains(contentStr, "Claude Test") { + t.Error("AGENTS.md does not contain Claude content") + } + if !strings.Contains(contentStr, "Cursor Test") { + t.Error("AGENTS.md does not contain Cursor content") + } +} + +func TestExport(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "export-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Change to temp directory + oldWd, _ := os.Getwd() + defer os.Chdir(oldWd) + os.Chdir(tmpDir) + + // Create AGENTS.md with test content + testContent := "# Test Agent Rules\n\nSome rules here." + os.WriteFile("AGENTS.md", []byte(testContent), 0644) + + // Test export to Gemini + err = Export("Gemini") + if err != nil { + t.Errorf("Export to Gemini failed: %v", err) + } + + // Check if GEMINI.md was created + content, err := os.ReadFile("GEMINI.md") + if err != nil { + t.Error("GEMINI.md was not created") + } + if string(content) != testContent { + t.Error("GEMINI.md content does not match AGENTS.md") + } + + // Test export to Claude + err = Export("Claude") + if err != nil { + t.Errorf("Export to Claude failed: %v", err) + } + + // Check if CLAUDE.md was created + content, err = os.ReadFile("CLAUDE.md") + if err != nil { + t.Error("CLAUDE.md was not created") + } + if string(content) != testContent { + t.Error("CLAUDE.md content does not match AGENTS.md") + } + + // Test export to Windsurf + err = Export("Windsurf") + if err != nil { + t.Errorf("Export to Windsurf failed: %v", err) + } + + // Check if .windsurf/rules/AGENTS.md was created + content, err = os.ReadFile(filepath.Join(".windsurf", "rules", "AGENTS.md")) + if err != nil { + t.Error(".windsurf/rules/AGENTS.md was not created") + } + if string(content) != testContent { + t.Error(".windsurf/rules/AGENTS.md content does not match AGENTS.md") + } + + // Test export with unknown agent + err = Export("UnknownAgent") + if err == nil { + t.Error("Expected error for unknown agent") + } +} + +func TestPrompt(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "prompt-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + // Change to temp directory + oldWd, _ := os.Getwd() + defer os.Chdir(oldWd) + os.Chdir(tmpDir) + + // Create .prompts directory with a test file + os.MkdirAll(".prompts", 0755) + os.WriteFile(filepath.Join(".prompts", "test.md"), []byte("# Test Prompt"), 0644) + + // Run prompt command - it should not error + err = Prompt() + if err != nil { + t.Errorf("Prompt failed: %v", err) + } +} diff --git a/internal/commands/prompt.go b/internal/commands/prompt.go index fccd746..73d721e 100644 --- a/internal/commands/prompt.go +++ b/internal/commands/prompt.go @@ -9,7 +9,8 @@ import ( // Prompt prints the aggregated prompt from all rule files func Prompt() error { - fmt.Println("# Aggregated Prompt from All Rules\n") + fmt.Println("# Aggregated Prompt from All Rules") + fmt.Println() // Get paths from the default agent (normalized format) paths, err := agents.GetRulePaths(agents.Default) From b8350a518bd5c8ae2420de06015525a47a488b2f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 2 Nov 2025 18:21:19 +0000 Subject: [PATCH 4/4] Address code review comments - improve path handling and export function clarity Co-authored-by: alexec <1142830+alexec@users.noreply.github.com> --- internal/commands/export.go | 25 ++++++++++++++----------- internal/commands/import.go | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/internal/commands/export.go b/internal/commands/export.go index f0b483b..b6fc4c0 100644 --- a/internal/commands/export.go +++ b/internal/commands/export.go @@ -83,6 +83,14 @@ func exportToClaude(content []byte) error { return os.WriteFile(path, content, 0644) } +func exportToCodex(content []byte) error { + // Codex uses AGENTS.md which is already the normalized format + // No need to write - AGENTS.md is the source file + path := "AGENTS.md" + fmt.Printf(" Using existing %s (Codex uses the normalized format)\n", path) + return nil +} + func exportToGemini(content []byte) error { // Export to ./GEMINI.md path := "GEMINI.md" @@ -90,17 +98,11 @@ func exportToGemini(content []byte) error { return os.WriteFile(path, content, 0644) } -func exportToCodex(content []byte) error { - // Export to ./AGENTS.md (same as normalized) - path := "AGENTS.md" - fmt.Printf(" Using existing %s\n", path) - return nil -} - func exportToCursor(content []byte) error { - // Export to ./AGENTS.md (compatibility mode) + // Cursor uses AGENTS.md for compatibility mode + // No need to write - AGENTS.md is the source file path := "AGENTS.md" - fmt.Printf(" Using existing %s\n", path) + fmt.Printf(" Using existing %s (Cursor compatibility mode)\n", path) return nil } @@ -149,8 +151,9 @@ func exportToWindsurf(content []byte) error { } func exportToGoose(content []byte) error { - // Export to ./AGENTS.md (same as normalized) + // Goose uses AGENTS.md which is the normalized format + // No need to write - AGENTS.md is the source file path := "AGENTS.md" - fmt.Printf(" Using existing %s\n", path) + fmt.Printf(" Using existing %s (Goose uses the normalized format)\n", path) return nil } diff --git a/internal/commands/import.go b/internal/commands/import.go index 3767d85..4f5d30a 100644 --- a/internal/commands/import.go +++ b/internal/commands/import.go @@ -21,7 +21,7 @@ func Import() error { if err != nil { return fmt.Errorf("failed to get current directory: %w", err) } - agentsFilePath = agentsFilePath + "/AGENTS.md" + agentsFilePath = filepath.Join(agentsFilePath, "AGENTS.md") f, err := os.OpenFile(agentsFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) if err != nil {