|
| 1 | +# AGENTS.md - Opencoder Development Guide |
| 2 | + |
| 3 | +This file provides instructions for AI coding agents working in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +Opencoder is a native CLI application written in **Zig** that runs OpenCode CLI in a fully autonomous development loop. It creates plans and executes them continuously without stopping. |
| 8 | + |
| 9 | +- **Language**: Zig (0.14.0+ required, 0.15.2 used in CI) |
| 10 | +- **Dependencies**: Zig standard library only (no external dependencies) |
| 11 | +- **Build System**: Zig build system (`build.zig`) |
| 12 | + |
| 13 | +## Build Commands |
| 14 | + |
| 15 | +### Using Make (Recommended) |
| 16 | + |
| 17 | +```bash |
| 18 | +make # Build release version |
| 19 | +make test # Run all tests |
| 20 | +make lint # Format and check code |
| 21 | +make clean # Remove build artifacts |
| 22 | +make install # Install to /usr/local/bin (PREFIX configurable) |
| 23 | +``` |
| 24 | + |
| 25 | +### Using Zig Directly |
| 26 | + |
| 27 | +```bash |
| 28 | +# Build debug version |
| 29 | +zig build |
| 30 | + |
| 31 | +# Build release version |
| 32 | +zig build -Doptimize=ReleaseSafe |
| 33 | + |
| 34 | +# Run the application |
| 35 | +zig build run |
| 36 | + |
| 37 | +# Run with arguments |
| 38 | +zig build run -- --provider github --verbose |
| 39 | +``` |
| 40 | + |
| 41 | +## Testing |
| 42 | + |
| 43 | +```bash |
| 44 | +# Run all tests |
| 45 | +zig build test |
| 46 | + |
| 47 | +# Run tests for a specific module |
| 48 | +zig test src/config.zig |
| 49 | +zig test src/cli.zig |
| 50 | +zig test src/logger.zig |
| 51 | +zig test src/fs.zig |
| 52 | +zig test src/state.zig |
| 53 | +zig test src/plan.zig |
| 54 | +zig test src/executor.zig |
| 55 | +zig test src/evaluator.zig |
| 56 | +zig test src/loop.zig |
| 57 | +``` |
| 58 | + |
| 59 | +**Note**: Zig does not support running individual tests by name. Tests are per-file. |
| 60 | + |
| 61 | +## Linting and Formatting |
| 62 | + |
| 63 | +```bash |
| 64 | +# Check formatting (used in CI) |
| 65 | +zig fmt --check src/ |
| 66 | + |
| 67 | +# Auto-format code |
| 68 | +zig fmt src/ |
| 69 | + |
| 70 | +# Format a specific file |
| 71 | +zig fmt src/config.zig |
| 72 | +``` |
| 73 | + |
| 74 | +## Source Code Structure |
| 75 | + |
| 76 | +``` |
| 77 | +src/ |
| 78 | + main.zig # Entry point, CLI orchestration |
| 79 | + cli.zig # CLI argument parsing, help/usage text |
| 80 | + config.zig # Configuration, provider presets, env vars |
| 81 | + state.zig # Execution state persistence (JSON) |
| 82 | + fs.zig # File system utilities |
| 83 | + logger.zig # Logging infrastructure |
| 84 | + plan.zig # Plan parsing, validation, markdown handling |
| 85 | + executor.zig # OpenCode CLI process execution |
| 86 | + evaluator.zig # Plan completion evaluation |
| 87 | + loop.zig # Main autonomous execution loop |
| 88 | +``` |
| 89 | + |
| 90 | +## Code Style Guidelines |
| 91 | + |
| 92 | +### Imports |
| 93 | + |
| 94 | +1. Standard library import always first: `const std = @import("std");` |
| 95 | +2. Extract commonly used type aliases after std import |
| 96 | +3. Internal module imports follow, grouped logically |
| 97 | + |
| 98 | +```zig |
| 99 | +const std = @import("std"); |
| 100 | +const Allocator = std.mem.Allocator; |
| 101 | +
|
| 102 | +const config = @import("config.zig"); |
| 103 | +const Logger = @import("logger.zig").Logger; |
| 104 | +``` |
| 105 | + |
| 106 | +### Naming Conventions |
| 107 | + |
| 108 | +| Element | Convention | Example | |
| 109 | +|---------|------------|---------| |
| 110 | +| Files | snake_case | `config.zig`, `fs.zig` | |
| 111 | +| Types/Structs | PascalCase | `Logger`, `State`, `ExecutionResult` | |
| 112 | +| Functions | camelCase | `runPlanning`, `markTaskComplete` | |
| 113 | +| Constants | snake_case | `version`, `defaults` | |
| 114 | +| Module variables | snake_case | `shutdown_requested` | |
| 115 | + |
| 116 | +### Struct Patterns |
| 117 | + |
| 118 | +```zig |
| 119 | +pub const MyStruct = struct { |
| 120 | + field: Type, |
| 121 | + allocator: Allocator, |
| 122 | +
|
| 123 | + /// Initialize - returns struct value |
| 124 | + pub fn init(allocator: Allocator) MyStruct { |
| 125 | + return MyStruct{ |
| 126 | + .field = value, |
| 127 | + .allocator = allocator, |
| 128 | + }; |
| 129 | + } |
| 130 | +
|
| 131 | + /// Deinit - takes pointer for cleanup |
| 132 | + pub fn deinit(self: *MyStruct) void { |
| 133 | + // cleanup |
| 134 | + } |
| 135 | +
|
| 136 | + /// Methods that mutate take pointer |
| 137 | + pub fn mutate(self: *MyStruct) void { |
| 138 | + self.field = new_value; |
| 139 | + } |
| 140 | +
|
| 141 | + /// Read-only methods take value |
| 142 | + pub fn getValue(self: MyStruct) Type { |
| 143 | + return self.field; |
| 144 | + } |
| 145 | +}; |
| 146 | +``` |
| 147 | + |
| 148 | +### Error Handling |
| 149 | + |
| 150 | +- Define custom error sets as enums: `pub const ParseError = error{ InvalidArg, MissingValue };` |
| 151 | +- Use `try` for error propagation |
| 152 | +- Use `catch` with labeled blocks for explicit handling |
| 153 | +- Use `errdefer` for cleanup on error paths |
| 154 | + |
| 155 | +```zig |
| 156 | +const result = fsutil.readFile(path, allocator) catch |err| { |
| 157 | + if (err == error.FileNotFound) { |
| 158 | + return null; |
| 159 | + } |
| 160 | + return err; |
| 161 | +}; |
| 162 | +``` |
| 163 | + |
| 164 | +### Memory Management |
| 165 | + |
| 166 | +- Functions accept `Allocator` parameter when allocation is needed |
| 167 | +- Use `defer` for cleanup: `defer allocator.free(content);` |
| 168 | +- Use `errdefer` for cleanup that should only run on error |
| 169 | + |
| 170 | +```zig |
| 171 | +pub fn process(allocator: Allocator) ![]u8 { |
| 172 | + const data = try allocator.alloc(u8, 1024); |
| 173 | + errdefer allocator.free(data); // Only frees if error occurs |
| 174 | +
|
| 175 | + // ... work with data ... |
| 176 | +
|
| 177 | + return data; // Caller owns memory |
| 178 | +} |
| 179 | +``` |
| 180 | + |
| 181 | +### Documentation |
| 182 | + |
| 183 | +- File-level doc comments: `//!` at top of file |
| 184 | +- Public API doc comments: `///` before declarations |
| 185 | +- Inline comments: `//` |
| 186 | + |
| 187 | +```zig |
| 188 | +//! Module description goes here. |
| 189 | +//! Additional context about the module. |
| 190 | +
|
| 191 | +/// Describe what this function does. |
| 192 | +/// Explain parameters and return value. |
| 193 | +pub fn myFunction() void {} |
| 194 | +``` |
| 195 | + |
| 196 | +### Testing |
| 197 | + |
| 198 | +Tests go at the bottom of each file, separated by a comment block: |
| 199 | + |
| 200 | +```zig |
| 201 | +// ============================================================================ |
| 202 | +// Tests |
| 203 | +// ============================================================================ |
| 204 | +
|
| 205 | +test "descriptive test name" { |
| 206 | + const allocator = std.testing.allocator; |
| 207 | +
|
| 208 | + // Setup |
| 209 | + const result = try myFunction(allocator); |
| 210 | + defer allocator.free(result); |
| 211 | +
|
| 212 | + // Assertions |
| 213 | + try std.testing.expectEqual(expected, result); |
| 214 | + try std.testing.expectEqualStrings("expected", actual); |
| 215 | + try std.testing.expect(condition); |
| 216 | + try std.testing.expectError(error.Expected, errorFn()); |
| 217 | +} |
| 218 | +``` |
| 219 | + |
| 220 | +### String Handling |
| 221 | + |
| 222 | +- Multi-line strings use `\\` syntax |
| 223 | +- Buffer formatting with `std.fmt.bufPrint` |
| 224 | +- Dynamic strings with `std.ArrayListUnmanaged(u8)` |
| 225 | + |
| 226 | +```zig |
| 227 | +// Fixed buffer formatting |
| 228 | +var buf: [64]u8 = undefined; |
| 229 | +const msg = std.fmt.bufPrint(&buf, "Value: {d}", .{value}) catch "fallback"; |
| 230 | +
|
| 231 | +// Dynamic string building |
| 232 | +var list = std.ArrayListUnmanaged(u8){}; |
| 233 | +defer list.deinit(allocator); |
| 234 | +try list.appendSlice(allocator, "hello"); |
| 235 | +``` |
| 236 | + |
| 237 | +### Enum Patterns |
| 238 | + |
| 239 | +Use `StaticStringMap` for string-to-enum conversion: |
| 240 | + |
| 241 | +```zig |
| 242 | +pub const Phase = enum { |
| 243 | + planning, |
| 244 | + execution, |
| 245 | +
|
| 246 | + pub fn fromString(str: []const u8) ?Phase { |
| 247 | + const map = std.StaticStringMap(Phase).initComptime(.{ |
| 248 | + .{ "planning", .planning }, |
| 249 | + .{ "execution", .execution }, |
| 250 | + }); |
| 251 | + return map.get(str); |
| 252 | + } |
| 253 | +
|
| 254 | + pub fn toString(self: Phase) []const u8 { |
| 255 | + return switch (self) { |
| 256 | + .planning => "planning", |
| 257 | + .execution => "execution", |
| 258 | + }; |
| 259 | + } |
| 260 | +}; |
| 261 | +``` |
| 262 | + |
| 263 | +## CI Pipeline |
| 264 | + |
| 265 | +The GitHub Actions CI (`.github/workflows/ci.yml`) runs: |
| 266 | +1. Build on Ubuntu and macOS |
| 267 | +2. Run all unit tests |
| 268 | +3. Check code formatting with `zig fmt --check src/` |
| 269 | + |
| 270 | +Always ensure `zig fmt --check src/` passes before committing. |
0 commit comments