Skip to content

Commit 49a104f

Browse files
committed
v0.5.1 — AGENTS.md project file support
kode now auto-loads AGENTS.md from the working directory and appends its content to the system prompt with a "Project Instructions" header. - LoadProjectFile() reads AGENTS.md from cwd (silently absent) - Appended to SystemMessage in kode.New() with clear section marker - --no-agents CLI flag to skip (or Config.NoProjectFile = true) - Works for both CLI and programmatic API users
1 parent 0a8b7cd commit 49a104f

5 files changed

Lines changed: 198 additions & 1 deletion

File tree

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ kode run --model gpt-4o "Write a Go test for the loop engine"
8787
| `--max-iter <n>` | int | `90` | Maximum think→act cycles before giving up |
8888
| `--thinking <level>` | string | profile default | Reasoning depth — see [Thinking Levels](#thinking-levels). Leave empty for profile/provider default. |
8989
| `--sandbox` | bool | false | Run all shell commands inside an isolated Docker container |
90+
| `--no-color` | bool | false | Disable colored terminal output |
91+
| `--no-agents` | bool | false | Skip loading AGENTS.md from working directory |
9092
| `--system <prompt>` | string | built-in | Override the system prompt |
9193

9294
### Examples
@@ -494,6 +496,37 @@ kode run --system "Answer with only the code. No explanations." "Sort a slice of
494496

495497
---
496498

499+
## Project Instructions (AGENTS.md)
500+
501+
kode automatically loads `AGENTS.md` from the working directory and appends it to the system prompt with a `# Project Instructions` header. Use this file to document project conventions, architecture, style rules, or any context the agent should know.
502+
503+
```markdown
504+
# Project Conventions
505+
506+
- Use tabs, not spaces
507+
- Module: github.com/myorg/myproject
508+
- All errors must be handled, never ignored
509+
- Go 1.24+, stdlib only
510+
```
511+
512+
### How it works
513+
514+
1. When `kode.New()` is called, kode looks for `AGENTS.md` in `os.Getwd()`
515+
2. If found, the content is **appended** to the system message
516+
3. The default system prompt is preserved — `AGENTS.md` adds project context on top
517+
4. Programmatic API users also get this automatically
518+
519+
### Skipping AGENTS.md
520+
521+
```bash
522+
# Ignore AGENTS.md for this one-off task
523+
kode run --no-agents "Quick status check"
524+
```
525+
526+
Set `Config.NoProjectFile = true` to skip programmatically.
527+
528+
---
529+
497530
## Architecture
498531

499532
```
@@ -568,7 +601,7 @@ Requires Go 1.24+. Zero external test dependencies — tests use `httptest`, `te
568601

569602
| Package | Tests | Focus |
570603
|---------|-------|-------|
571-
| `kode` | 25 | Config defaults, API key fallback, thinking passthrough, system message, model profiles, lookup, label, timeout |
604+
| `kode` | 31 | Config defaults, API key fallback, thinking passthrough, system message, model profiles, lookup, label, timeout, project file (AGENTS.md) |
572605
| `internal/llm` | 14 | JSON marshaling, thinking/reasoning_effort fields, response parsing, custom timeout |
573606
| `internal/loop` | 7 | ReAct engine with httptest mock (simple answer, tool calls, max iter, cancellation) |
574607
| `internal/tool` | 7 | Registry CRUD, Get (found/not found), duplicate detection |

cmd/kode/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type runFlags struct {
5555
MaxIter int
5656
Sandbox bool
5757
NoColor bool
58+
NoAgents bool
5859
Task string
5960
}
6061

@@ -88,6 +89,9 @@ func parseRunFlags(args []string) (runFlags, error) {
8889
case "--no-color":
8990
f.NoColor = true
9091
i++
92+
case "--no-agents":
93+
f.NoAgents = true
94+
i++
9195
default:
9296
// Not a flag — treat remaining as the task
9397
goto done
@@ -117,6 +121,7 @@ Flags:
117121
Empty = profile default = provider default.
118122
--sandbox Run in isolated Docker container
119123
--no-color Disable colored terminal output
124+
--no-agents Skip loading AGENTS.md from working directory
120125
--system <prompt> System prompt override`)
121126
}
122127

@@ -157,6 +162,7 @@ func run(args []string) error {
157162
BaseURL: f.BaseURL,
158163
MaxIterations: f.MaxIter,
159164
SystemMessage: f.System,
165+
NoProjectFile: f.NoAgents,
160166
Thinking: f.Thinking,
161167
Tools: tools,
162168
SandboxCleanup: sandboxCleanup,

cmd/kode/main_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ func TestPrintUsage(t *testing.T) {
200200
"--thinking",
201201
"profile default",
202202
"--sandbox",
203+
"--no-color",
204+
"--no-agents",
203205
"--system",
204206
}
205207
for _, req := range required {

kode.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,15 @@ type Config struct {
6969
MaxIterations int
7070

7171
// SystemMessage is the system prompt injected at the start of every run.
72+
// If AGENTS.md exists in the working directory, its content is appended
73+
// automatically. Set NoProjectFile to true to skip this.
7274
SystemMessage string
7375

76+
// NoProjectFile disables automatic loading of AGENTS.md from the
77+
// working directory. By default, kode reads AGENTS.md and appends
78+
// its content to the system message with a "Project Instructions" header.
79+
NoProjectFile bool
80+
7481
// SandboxCleanup, if set, is called by Agent.Close() to destroy the
7582
// Docker sandbox container. Set by the CLI when --sandbox is active.
7683
SandboxCleanup func() error
@@ -179,6 +186,25 @@ func ProfileLabel(model string) string {
179186
return model
180187
}
181188

189+
// ── Project File (AGENTS.md) ─────────────────────────────────────────
190+
191+
// ProjectFileName is the name of the project-level instructions file
192+
// that kode automatically loads from the working directory.
193+
const ProjectFileName = "AGENTS.md"
194+
195+
// LoadProjectFile reads ProjectFileName from the current working directory.
196+
// Returns the file content (trimmed) if it exists and is readable.
197+
// Returns empty string if the file doesn't exist or can't be read.
198+
// The content is intended to be appended to the system message with a
199+
// clear header — use it for project conventions, architecture notes, etc.
200+
func LoadProjectFile() string {
201+
data, err := os.ReadFile(ProjectFileName)
202+
if err != nil {
203+
return ""
204+
}
205+
return strings.TrimSpace(string(data))
206+
}
207+
182208
// ── Defaults ──────────────────────────────────────────────────────────
183209

184210
const (
@@ -240,6 +266,17 @@ func New(cfg Config) (*Agent, error) {
240266
tools[i] = &toolAdapter{t}
241267
}
242268

269+
// Load AGENTS.md from the working directory and append to system message
270+
if !cfg.NoProjectFile {
271+
if projectContent := LoadProjectFile(); projectContent != "" {
272+
if cfg.SystemMessage != "" {
273+
cfg.SystemMessage += "\n\n# Project Instructions\n\n" + projectContent
274+
} else {
275+
cfg.SystemMessage = "# Project Instructions\n\n" + projectContent
276+
}
277+
}
278+
}
279+
243280
registry := tool.NewRegistry(tools)
244281
client := llm.New(cfg.BaseURL, cfg.APIKey, cfg.Model, cfg.Thinking, time.Duration(timeout)*time.Second)
245282
engine := loop.New(client, registry, cfg.MaxIterations, cfg.SystemMessage, cfg.Renderer, maxContext)

kode_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"os"
7+
"strings"
78
"testing"
89
)
910

@@ -525,3 +526,121 @@ func TestProfileMaxContext_Unknown(t *testing.T) {
525526
t.Errorf("LookupProfile for unknown model = %v, want nil", p)
526527
}
527528
}
529+
530+
531+
// ── Project File (AGENTS.md) Tests ───────────────────────────────────
532+
533+
func TestLoadProjectFile_Missing(t *testing.T) {
534+
// No AGENTS.md in current dir — should return empty
535+
content := LoadProjectFile()
536+
if content != "" {
537+
t.Errorf("LoadProjectFile() with no file = %q, want empty", content)
538+
}
539+
}
540+
541+
func TestLoadProjectFile_WithFile(t *testing.T) {
542+
dir := t.TempDir()
543+
cwd, _ := os.Getwd()
544+
os.Chdir(dir)
545+
defer os.Chdir(cwd)
546+
547+
if err := os.WriteFile("AGENTS.md", []byte("This project uses Go 1.24."), 0644); err != nil {
548+
t.Fatal(err)
549+
}
550+
551+
content := LoadProjectFile()
552+
if content != "This project uses Go 1.24." {
553+
t.Errorf("LoadProjectFile() = %q, want %q", content, "This project uses Go 1.24.")
554+
}
555+
}
556+
557+
func TestLoadProjectFile_TrimsWhitespace(t *testing.T) {
558+
dir := t.TempDir()
559+
cwd, _ := os.Getwd()
560+
os.Chdir(dir)
561+
defer os.Chdir(cwd)
562+
563+
if err := os.WriteFile("AGENTS.md", []byte(" \n project instructions \n "), 0644); err != nil {
564+
t.Fatal(err)
565+
}
566+
567+
content := LoadProjectFile()
568+
if content != "project instructions" {
569+
t.Errorf("LoadProjectFile() = %q, want %q", content, "project instructions")
570+
}
571+
}
572+
573+
func TestNew_ProjectFileAppended(t *testing.T) {
574+
dir := t.TempDir()
575+
cwd, _ := os.Getwd()
576+
os.Chdir(dir)
577+
defer os.Chdir(cwd)
578+
579+
if err := os.WriteFile("AGENTS.md", []byte("Use tabs, not spaces."), 0644); err != nil {
580+
t.Fatal(err)
581+
}
582+
583+
cfg := Config{
584+
APIKey: "sk-test",
585+
SystemMessage: "You are a bot.",
586+
}
587+
agent, err := New(cfg)
588+
if err != nil {
589+
t.Fatal(err)
590+
}
591+
if !strings.Contains(agent.config.SystemMessage, "Use tabs, not spaces.") {
592+
t.Errorf("SystemMessage should contain AGENTS.md content, got: %q", agent.config.SystemMessage)
593+
}
594+
if !strings.Contains(agent.config.SystemMessage, "Project Instructions") {
595+
t.Errorf("SystemMessage should have 'Project Instructions' header, got: %q", agent.config.SystemMessage)
596+
}
597+
if !strings.Contains(agent.config.SystemMessage, "You are a bot.") {
598+
t.Errorf("SystemMessage should keep original content, got: %q", agent.config.SystemMessage)
599+
}
600+
}
601+
602+
func TestNew_ProjectFileWithNoOriginalSystem(t *testing.T) {
603+
dir := t.TempDir()
604+
cwd, _ := os.Getwd()
605+
os.Chdir(dir)
606+
defer os.Chdir(cwd)
607+
608+
if err := os.WriteFile("AGENTS.md", []byte("Just these instructions."), 0644); err != nil {
609+
t.Fatal(err)
610+
}
611+
612+
cfg := Config{
613+
APIKey: "sk-test",
614+
}
615+
agent, err := New(cfg)
616+
if err != nil {
617+
t.Fatal(err)
618+
}
619+
if agent.config.SystemMessage != "# Project Instructions\n\nJust these instructions." {
620+
t.Errorf("SystemMessage = %q, want 'Project Instructions' + content", agent.config.SystemMessage)
621+
}
622+
}
623+
624+
func TestNew_NoProjectFileOptOut(t *testing.T) {
625+
dir := t.TempDir()
626+
cwd, _ := os.Getwd()
627+
os.Chdir(dir)
628+
defer os.Chdir(cwd)
629+
630+
if err := os.WriteFile("AGENTS.md", []byte("Should not appear."), 0644); err != nil {
631+
t.Fatal(err)
632+
}
633+
634+
cfg := Config{
635+
APIKey: "sk-test",
636+
SystemMessage: "Only this.",
637+
NoProjectFile: true,
638+
}
639+
agent, err := New(cfg)
640+
if err != nil {
641+
t.Fatal(err)
642+
}
643+
if agent.config.SystemMessage != "Only this." {
644+
t.Errorf("SystemMessage = %q, want original only (no project file)", agent.config.SystemMessage)
645+
}
646+
}

0 commit comments

Comments
 (0)