Skip to content

Commit fbadf37

Browse files
committed
feat: add AGENTS.md for agent self-maintenance + github_repo config injection
- Create AGENTS.md with comprehensive project context for agent self-maintenance (source layout, conventions, build/test, adding tools/flags/model profiles, security) - Add github_repo_directory and github_repo_url config fields (CLI flags, env vars, JSON config, system prompt injection) so the agent knows its own repository location - Fix TestLoadProjectFile_Missing to use temp dir (was failing with AGENTS.md present) - Fix TestConfigSystemMessage to use NoProjectFile: true All 16 test packages pass.
1 parent d00e871 commit fbadf37

8 files changed

Lines changed: 273 additions & 7 deletions

File tree

AGENTS.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# odek — Agent Maintenance Guide
2+
3+
This file is automatically loaded by odek when running inside this repository.
4+
It provides context about the project's architecture, conventions, and how to update/maintain it.
5+
6+
---
7+
8+
## Project Identity
9+
10+
- **Package:** `odek` (Go module: `github.com/BackendStack21/kode`)
11+
- **What it is:** Minimal Go autonomous agent runtime — ReAct (Reasoning + Acting) loop with zero external dependencies (stdlib only).
12+
- **Binary:** `odek` — single static binary, ~11 MB, instant startup.
13+
- **Config:** Layered priority: `~/.odek/config.json``./odek.json``ODEK_*` env vars → CLI flags.
14+
15+
## Source Layout
16+
17+
```
18+
odek.go Public API (Config, New, Run, Close, ModelProfile, KnownProfiles)
19+
odek_test.go Tests for public API
20+
cmd/odek/
21+
main.go CLI entry point, flag parsing, commands, sandbox
22+
shell.go Built-in shell tool (local or docker exec)
23+
serve.go Web UI server (HTTP + WebSocket)
24+
subagent.go Sub-agent command (--goal, --context, --task)
25+
subagent_tool.go delegate_tasks built-in tool
26+
ui/index.html Single-page Web UI (~770 LOC, vanilla JS + CSS)
27+
*_test.go CLI, subagent, contract, and E2E tests
28+
internal/
29+
config/loader.go Config file loading, env vars, priority merge
30+
llm/client.go OpenAI-compatible HTTP client
31+
loop/loop.go ReAct engine (observe → think → act → repeat)
32+
session/session.go Session store (CRUD, trim, cleanup)
33+
render/render.go Terminal output with model label and color
34+
resource/resource.go @-reference resolver (files, sessions)
35+
ws/ws.go RFC 6455 WebSocket framing (~200 LOC)
36+
tool/registry.go Thread-safe tool registry, clarify.go
37+
danger/classifier.go Command/URL classification for security gating
38+
memory/ MemoryManager (facts, buffer, episodes, merge, scan)
39+
skills/ Skill system (types, loader, triggers, self-improve, curator, import)
40+
telegram/ Telegram bot client, poller, handler, commands
41+
docs/ Documentation (CLI, API, CONFIG, MCP, MEMORY, etc.)
42+
```
43+
44+
## How It Works
45+
46+
1. **Agent Loop (`internal/loop/loop.go`):** ReAct cycle: observe → think → act → repeat.
47+
- LLM returns a tool call or final answer.
48+
- Tools execute and return results.
49+
- Max 90 iterations by default (`--max-iter`).
50+
2. **Tools:** Built-in: `read_file`, `write_file`, `search_files`, `patch`, `shell`, `browser`, `memory`, `clarify`, `delegate_tasks`. All gated by the `danger` security classifier.
51+
3. **Skills:** Trigger-matched `SKILL.md` files loaded on-demand. Auto-learns from patterns every session. Stored in `~/.odek/skills/` and `./.odek/skills/`.
52+
4. **Memory:** Three tiers — facts (durable entries), session buffer (turn summaries), episodes (LLM-extracted knowledge). Uses go-vector RandomProjections for merge-on-write.
53+
5. **Sub-agents:** `delegate_tasks` spawns real OS subprocesses via `odek subagent`. Each gets a fresh agent process with its own config.
54+
55+
## Key Conventions
56+
57+
- **Zero-dependency policy:** No external Go modules beyond stdlib. All contributions must maintain this.
58+
- **Tests:** 1760+ tests, run with `go test ./...` (no network, no Docker needed for unit tests).
59+
- **Error handling:** Return errors, don't panic. Fatal errors go through `fmt.Fprintf(os.Stderr, ...)`.
60+
- **Config structs:** JSON tags for serialization. `*bool` for optional tristate fields (nil = not set).
61+
- **Security:** Symlinked AGENTS.md is refused. Shell commands are classified into 8 risk classes. Sandbox mode uses Docker with no network and read-only mounts.
62+
- **Model profiles:** Added in `odek.go`'s `KnownProfiles` var. Longest-prefix matching.
63+
64+
## How to Update/Maintain This Project
65+
66+
### Building
67+
68+
```bash
69+
go build -o bin/odek ./cmd/odek # build binary
70+
go build ./... # check compilation
71+
```
72+
73+
### Testing
74+
75+
```bash
76+
go test ./... -count=1 # all unit tests
77+
go test -race ./... -count=1 # with race detector
78+
go test -v ./internal/session/ # specific package
79+
KODE_E2E=true go test ./cmd/odek/ -run "TestE2E_" # E2E tests
80+
```
81+
82+
### Adding a New Model Profile
83+
84+
Edit `odek.go`, add to `KnownProfiles`:
85+
```go
86+
{
87+
Prefix: "my-model",
88+
Profile: ModelProfile{
89+
Label: "My Model",
90+
DefaultThinking: "enabled",
91+
Timeout: 120,
92+
MaxContext: 128_000,
93+
},
94+
},
95+
```
96+
97+
### Adding a New Tool
98+
99+
1. Create the tool struct in `cmd/odek/` or `internal/tool/`.
100+
2. Implement `Name()`, `Description()`, `Schema()`, `Call(args string) (string, error)`.
101+
3. Register in `registerBuiltinTools()` in `cmd/odek/main.go`.
102+
4. Add security classification in `internal/danger/classifier.go` if it runs shell commands.
103+
5. Add tests.
104+
105+
### Adding a New Command/Flag
106+
107+
1. Define the flag in `main.go`'s `flags` struct.
108+
2. Parse in `parseFlags()`.
109+
3. Wire through `resolveConfig()` into the resolved config.
110+
4. Pass to `odek.New()` or the relevant handler.
111+
5. Document in `docs/CLI.md` and `docs/CONFIG.md`.
112+
6. Add env var support in `internal/config/loader.go`.
113+
114+
### Documentation
115+
116+
- All docs are Markdown in `/docs/`.
117+
- `docs/CLI.md` — CLI reference (commands, flags, error codes).
118+
- `docs/API.md` — Programmatic API with Go examples.
119+
- `docs/CONFIG.md` — Configuration system.
120+
- `docs/DEVELOPMENT.md` — Building, testing, contributing.
121+
- Update docs when adding/changing features.
122+
123+
### Skill System Maintenance
124+
125+
- Skills live in `~/.odek/skills/` and `./.odek/skills/` as `SKILL.md` files with YAML frontmatter.
126+
- Learning is on by default (`--no-learn` to disable).
127+
- Use `odek skill curate` to audit quality/overlap.
128+
- Use `odek skill import <uri>` to import from URLs.
129+
130+
### Memory System
131+
132+
- Facts: `~/.odek/memory/user.md` and `~/.odek/memory/env.md`.
133+
- Episodes: Stored as JSON files in `~/.odek/memory/episodes/`.
134+
- Buffer: Ring buffer of turn summaries, stored in `~/.odek/memory/buffer.json`.
135+
- Use the `memory` tool (6 actions: read, add, replace, remove, consolidate, search).
136+
137+
### Security Considerations
138+
139+
- **Prompt injection:** Identity anchoring anchors agent identity at the start of the system prompt. Anti-injection rules applied on top. AGENTS.md comes after identity.
140+
- **Symlink protection:** `LoadProjectFile()` refuses symlinks.
141+
- **Sandbox:** `--sandbox` flag runs in Docker with no network, no host mounts beyond working dir, zero capabilities.
142+
- **Danger classifier:** Commands classified into 8 risk classes. Configurable via `dangerous` config section.
143+
144+
### Release Process
145+
146+
1. Update version tag: `git tag v0.x.y`
147+
2. Build cross-platform: `make build-all`
148+
3. Run full test suite: `make test-all`
149+
4. Push tag: `git push origin v0.x.y`
150+
151+
## Common Gotchas
152+
153+
- When modifying `Config` struct in `odek.go`, also update `internal/config/loader.go` (CLI flags, env vars, JSON serialization).
154+
- When adding to KnownProfiles, ensure tests in `odek_test.go` cover the new profile.
155+
- The Web UI (`cmd/odek/ui/index.html`) is embedded — rebuild the binary to see UI changes.
156+
- Session files are JSON in `~/.odek/sessions/` — corrupt data is handled gracefully with fallback scan.
157+
- The Telegram bot uses long-polling (no webhooks), built on stdlib `net/http`.
158+
- `delegate_tasks` sub-agents have a 120s default timeout. Override via `subagent.timeout_seconds` config.

cmd/odek/main.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ type runFlags struct {
192192
SandboxCPUs string // CPU limit (e.g. "0.5", "2")
193193
SandboxUser string // Container user (e.g. "1000:1000")
194194
SandboxReadonly *bool // nil = not set; true = read-only mount
195+
196+
// Repo context flags
197+
GithubRepoDirectory string // --github-repo-dir
198+
GithubRepoUrl string // --github-repo-url
195199
}
196200

197201
// parseRunFlags parses `odek run` arguments and returns the parsed flags.
@@ -257,6 +261,12 @@ func parseRunFlags(args []string) (runFlags, error) {
257261
case "--sandbox-user":
258262
f.SandboxUser = args[i+1]
259263
i += 2
264+
case "--github-repo-dir":
265+
f.GithubRepoDirectory = args[i+1]
266+
i += 2
267+
case "--github-repo-url":
268+
f.GithubRepoUrl = args[i+1]
269+
i += 2
260270
case "--ctx", "-c":
261271
f.Ctx = strings.Split(args[i+1], ",")
262272
i += 2
@@ -463,6 +473,7 @@ const defaultConfigTemplate = `{
463473
"no_color": false,
464474
"no_agents": false,
465475
"system": "",
476+
"github_repo_directory": "",
466477
"sandbox_image": "",
467478
"sandbox_network": "bridge",
468479
"sandbox_readonly": false,
@@ -588,6 +599,7 @@ func initConfig(args []string) error {
588599
fmt.Println(" no_color Disable colored output (true/false)")
589600
fmt.Println(" no_agents Skip AGENTS.md (true/false)")
590601
fmt.Println(" system System prompt override")
602+
fmt.Println(" github_repo_directory Local clone path of the project repo")
591603
fmt.Println(" sandbox_image Docker image (alpine:latest if empty)")
592604
fmt.Println(" sandbox_network Network mode (bridge | none | host)")
593605
fmt.Println(" sandbox_readonly Mount working directory read-only")
@@ -719,6 +731,9 @@ func run(args []string) error {
719731
SandboxMemory: f.SandboxMemory,
720732
SandboxCPUs: f.SandboxCPUs,
721733
SandboxUser: f.SandboxUser,
734+
735+
GithubRepoDirectory: f.GithubRepoDirectory,
736+
GithubRepoUrl: f.GithubRepoUrl,
722737
})
723738

724739
// Resolve @references and --ctx file attachments in the task
@@ -734,8 +749,14 @@ func run(args []string) error {
734749
if systemMessage == "" {
735750
systemMessage = defaultSystem
736751
}
752+
if resolved.GithubRepoDirectory != "" {
753+
systemMessage += fmt.Sprintf("\n\nRepository directory: %s\nThis is the local clone of the project repository. You can read and modify files here.", resolved.GithubRepoDirectory)
754+
}
755+
if resolved.GithubRepoUrl != "" {
756+
systemMessage += fmt.Sprintf("\nRepository URL: %s\nThis is the upstream GitHub repository.", resolved.GithubRepoUrl)
757+
}
737758

738-
// Build sandbox config from resolved settings
759+
// Build sandbox config from resolved settings (first occurrence)
739760
sbCfg := sandboxConfig{
740761
Image: resolved.SandboxImage,
741762
Network: resolved.SandboxNetwork,
@@ -1529,8 +1550,14 @@ func continueCmd(args []string) error {
15291550
if systemMessage == "" {
15301551
systemMessage = defaultSystem
15311552
}
1553+
if resolved.GithubRepoDirectory != "" {
1554+
systemMessage += fmt.Sprintf("\n\nRepository directory: %s\nThis is the local clone of the project repository.", resolved.GithubRepoDirectory)
1555+
}
1556+
if resolved.GithubRepoUrl != "" {
1557+
systemMessage += fmt.Sprintf("\nRepository URL: %s\nThis is the upstream GitHub repository.", resolved.GithubRepoUrl)
1558+
}
15321559

1533-
// Sandbox (if enabled in config)
1560+
// Sandbox (if enabled in config) (second occurrence)
15341561
if resolved.Sandbox {
15351562
sbCfg := sandboxConfig{
15361563
Image: resolved.SandboxImage,

cmd/odek/mcp.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,14 @@ Flags:
5656
if systemMessage == "" {
5757
systemMessage = defaultSystem
5858
}
59+
if resolved.GithubRepoDirectory != "" {
60+
systemMessage += fmt.Sprintf("\n\nRepository directory: %s\nThis is the local clone of the project repository. You can read and modify files here.", resolved.GithubRepoDirectory)
61+
}
62+
if resolved.GithubRepoUrl != "" {
63+
systemMessage += fmt.Sprintf("\nRepository URL: %s\nThis is the upstream GitHub repository.", resolved.GithubRepoUrl)
64+
}
5965

60-
// Build sandbox config from resolved settings
66+
// Start agent loop (mcp)
6167
sbCfg := sandboxConfig{
6268
Image: resolved.SandboxImage,
6369
Network: resolved.SandboxNetwork,

cmd/odek/repl.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,14 @@ func replCmd(args []string) error {
6262
if systemMessage == "" {
6363
systemMessage = defaultSystem
6464
}
65+
if resolved.GithubRepoDirectory != "" {
66+
systemMessage += fmt.Sprintf("\n\nRepository directory: %s\nThis is the local clone of the project repository. You can read and modify files here. When asked to update your own code, this is where the source lives.", resolved.GithubRepoDirectory)
67+
}
68+
if resolved.GithubRepoUrl != "" {
69+
systemMessage += fmt.Sprintf("\nRepository URL: %s\nThis is the upstream GitHub repository.", resolved.GithubRepoUrl)
70+
}
6571

66-
// Auto-apply sandbox if resuming a sandboxed session
72+
// session resume
6773
if sess != nil && sess.Sandbox && !resolved.Sandbox {
6874
resolved.Sandbox = true
6975
fmt.Fprintf(os.Stderr, "odek: session was sandboxed — enabling sandbox\n")

cmd/odek/serve.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,14 @@ func serveCmd(args []string) error {
9696
if systemMessage == "" {
9797
systemMessage = defaultSystem
9898
}
99+
if resolved.GithubRepoDirectory != "" {
100+
systemMessage += fmt.Sprintf("\n\nRepository directory: %s\nThis is the local clone of the project repository.", resolved.GithubRepoDirectory)
101+
}
102+
if resolved.GithubRepoUrl != "" {
103+
systemMessage += fmt.Sprintf("\nRepository URL: %s\nThis is the upstream GitHub repository.", resolved.GithubRepoUrl)
104+
}
99105

106+
// Build sandbox config from resolved settings (serve)
100107
store, err := session.NewStore()
101108
if err != nil {
102109
return fmt.Errorf("session store: %w", err)

cmd/odek/telegram.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,14 @@ func telegramCmd(args []string) error {
118118
if systemMessage == "" {
119119
systemMessage = defaultSystem
120120
}
121+
if resolved.GithubRepoDirectory != "" {
122+
systemMessage += fmt.Sprintf("\n\nRepository directory: %s\nThis is the local clone of the project repository.", resolved.GithubRepoDirectory)
123+
}
124+
if resolved.GithubRepoUrl != "" {
125+
systemMessage += fmt.Sprintf("\nRepository URL: %s\nThis is the upstream GitHub repository.", resolved.GithubRepoUrl)
126+
}
121127

122-
// 10. Wire handler callbacks.
128+
// Telegram-specific system prompt additions
123129
//
124130
// Important: OnTextMessage processes in a background goroutine so it doesn't
125131
// block the main update processing loop. The TelegramApprover blocks waiting

0 commit comments

Comments
 (0)