Skip to content

Commit 7301d8e

Browse files
committed
feat: comprehensive improvements for robust evolution
1. Fixed agent tool format — clearer prompts with explicit tool examples 2. Smarter task selection — codebase analysis (TODOs, hotspots, no-test packages) 3. Error recovery — retry failed tasks with error context 4. Test coverage tracking — track coverage over time in memory/coverage_history.jsonl 5. Weekly summary — auto-generated stats and summary 6. Live stats — stats.json for site with commits, lines, tests 7. RSS feed — feed.xml for journal updates 8. Audit log — .iterate/audit.jsonl tracks all agent tool calls
1 parent b3133c5 commit 7301d8e

11 files changed

Lines changed: 582 additions & 25 deletions

File tree

internal/evolution/analysis.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package evolution
2+
3+
import (
4+
"os/exec"
5+
"path/filepath"
6+
"sort"
7+
"strings"
8+
)
9+
10+
// CodeAnalysis holds analysis results for the codebase.
11+
type CodeAnalysis struct {
12+
TODOs []string // file:line: content
13+
Hotspots []string // most changed files
14+
NoTestPkgs []string // packages without test files
15+
BuildOK bool
16+
TestOK bool
17+
}
18+
19+
// AnalyzeCodebase scans the repo for improvement opportunities.
20+
func AnalyzeCodebase(repoPath string) CodeAnalysis {
21+
analysis := CodeAnalysis{BuildOK: true, TestOK: true}
22+
23+
// Check build
24+
if err := exec.Command("go", "build", "./...").Run(); err != nil {
25+
analysis.BuildOK = false
26+
}
27+
28+
// Check tests
29+
if err := exec.Command("go", "test", "./...").Run(); err != nil {
30+
analysis.TestOK = false
31+
}
32+
33+
// Find TODOs/FIXMEs
34+
analysis.TODOs = findTODOs(repoPath)
35+
36+
// Find hotspots (most changed files)
37+
analysis.Hotspots = findHotspots(repoPath)
38+
39+
// Find packages without tests
40+
analysis.NoTestPkgs = findNoTestPackages(repoPath)
41+
42+
return analysis
43+
}
44+
45+
// FormatAnalysis returns a human-readable summary for the agent.
46+
func (a CodeAnalysis) FormatAnalysis() string {
47+
var sb strings.Builder
48+
49+
if !a.BuildOK {
50+
sb.WriteString("🔴 BUILD BROKEN — fix this first!\n\n")
51+
}
52+
if !a.TestOK {
53+
sb.WriteString("🔴 TESTS FAILING — fix this first!\n\n")
54+
}
55+
56+
if len(a.TODOs) > 0 {
57+
sb.WriteString("## TODOs found in code\n\n")
58+
limit := 10
59+
if len(a.TODOs) < limit {
60+
limit = len(a.TODOs)
61+
}
62+
for _, todo := range a.TODOs[:limit] {
63+
sb.WriteString("- " + todo + "\n")
64+
}
65+
sb.WriteString("\n")
66+
}
67+
68+
if len(a.Hotspots) > 0 {
69+
sb.WriteString("## Most changed files (hotspots)\n\n")
70+
limit := 5
71+
if len(a.Hotspots) < limit {
72+
limit = len(a.Hotspots)
73+
}
74+
for _, h := range a.Hotspots[:limit] {
75+
sb.WriteString("- " + h + "\n")
76+
}
77+
sb.WriteString("\n")
78+
}
79+
80+
if len(a.NoTestPkgs) > 0 {
81+
sb.WriteString("## Packages without tests\n\n")
82+
for _, pkg := range a.NoTestPkgs {
83+
sb.WriteString("- " + pkg + "\n")
84+
}
85+
sb.WriteString("\n")
86+
}
87+
88+
return sb.String()
89+
}
90+
91+
func findTODOs(repoPath string) []string {
92+
out, err := exec.Command("grep", "-rn", "--include=*.go",
93+
"TODO\\|FIXME\\|HACK\\|XXX",
94+
filepath.Join(repoPath, "cmd"),
95+
filepath.Join(repoPath, "internal"),
96+
).CombinedOutput()
97+
if err != nil {
98+
return nil
99+
}
100+
101+
var results []string
102+
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
103+
if line == "" {
104+
continue
105+
}
106+
// Strip repo path prefix
107+
line = strings.TrimPrefix(line, repoPath+"/")
108+
results = append(results, line)
109+
}
110+
return results
111+
}
112+
113+
func findHotspots(repoPath string) []string {
114+
out, err := exec.Command("git", "-C", repoPath, "log", "--pretty=format:",
115+
"--name-only", "-50").CombinedOutput()
116+
if err != nil {
117+
return nil
118+
}
119+
120+
counts := map[string]int{}
121+
for _, line := range strings.Split(string(out), "\n") {
122+
line = strings.TrimSpace(line)
123+
if line == "" || strings.HasSuffix(line, "_test.go") {
124+
continue
125+
}
126+
counts[line]++
127+
}
128+
129+
type entry struct {
130+
name string
131+
count int
132+
}
133+
var entries []entry
134+
for name, count := range counts {
135+
entries = append(entries, entry{name, count})
136+
}
137+
sort.Slice(entries, func(i, j int) bool {
138+
return entries[i].count > entries[j].count
139+
})
140+
141+
var results []string
142+
limit := 10
143+
if len(entries) < limit {
144+
limit = len(entries)
145+
}
146+
for _, e := range entries[:limit] {
147+
results = append(results, e.name+" (changed "+string(rune('0'+e.count))+"x)")
148+
}
149+
return results
150+
}
151+
152+
func findNoTestPackages(repoPath string) []string {
153+
out, err := exec.Command("go", "list", "./...").Output()
154+
if err != nil {
155+
return nil
156+
}
157+
158+
var noTest []string
159+
for _, pkg := range strings.Split(strings.TrimSpace(string(out)), "\n") {
160+
if pkg == "" || strings.Contains(pkg, "vendor") {
161+
continue
162+
}
163+
// Check if package has test files
164+
testOut, _ := exec.Command("go", "list", "-f", "{{.TestGoFiles}}", pkg).Output()
165+
if strings.TrimSpace(string(testOut)) == "[]" {
166+
noTest = append(noTest, pkg)
167+
}
168+
}
169+
return noTest
170+
}

internal/evolution/engine.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,3 +365,30 @@ func (e *Engine) handlePRReviewAndMerge(ctx context.Context, p iteragent.Provide
365365

366366
_ = e.switchToMain(ctx)
367367
}
368+
369+
// auditLog appends a tool call or error to .iterate/audit.jsonl for debugging.
370+
func (e *Engine) auditLog(eventType, tool, detail string) {
371+
auditPath := filepath.Join(e.repoPath, ".iterate", "audit.jsonl")
372+
_ = os.MkdirAll(filepath.Dir(auditPath), 0o755)
373+
374+
entry := map[string]string{
375+
"ts": time.Now().UTC().Format(time.RFC3339),
376+
"type": eventType,
377+
"tool": tool,
378+
}
379+
if detail != "" {
380+
// Truncate long details
381+
if len(detail) > 200 {
382+
detail = detail[:200] + "..."
383+
}
384+
entry["detail"] = detail
385+
}
386+
387+
data, _ := json.Marshal(entry)
388+
f, err := os.OpenFile(auditPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
389+
if err != nil {
390+
return
391+
}
392+
defer f.Close()
393+
f.Write(append(data, '\n'))
394+
}

internal/evolution/git.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,15 @@ func (e *Engine) runTool(ctx context.Context, name string, args map[string]strin
1919
if !ok {
2020
return "", fmt.Errorf("tool %q not found", name)
2121
}
22-
return tool.Execute(ctx, args)
22+
23+
// Audit log
24+
e.auditLog("tool_call", name, args["cmd"])
25+
26+
result, err := tool.Execute(ctx, args)
27+
if err != nil {
28+
e.auditLog("tool_error", name, err.Error())
29+
}
30+
return result, err
2331
}
2432

2533
func (e *Engine) hasChanges(ctx context.Context) (bool, error) {

internal/evolution/git_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ func initGitRepo(t *testing.T, dir string) {
3434
t.Fatalf("%v failed: %v\n%s", c.args, err, out)
3535
}
3636
}
37+
// Add .gitignore to exclude .iterate/ directory
38+
os.WriteFile(filepath.Join(dir, ".gitignore"), []byte(".iterate/\n"), 0o644)
3739
// initial commit so HEAD exists
3840
os.WriteFile(filepath.Join(dir, "README.md"), []byte("# test\n"), 0o644)
39-
cmd := exec.Command("git", "add", "README.md")
41+
cmd := exec.Command("git", "add", "README.md", ".gitignore")
4042
cmd.Dir = dir
4143
cmd.CombinedOutput()
4244
cmd = exec.Command("git", "commit", "-m", "init")
@@ -725,8 +727,8 @@ func TestHasChanges_WithIgnoredFiles(t *testing.T) {
725727
dir := t.TempDir()
726728
initGitRepo(t, dir)
727729

728-
// Create .gitignore
729-
os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.tmp\n"), 0o644)
730+
// Create .gitignore with both patterns
731+
os.WriteFile(filepath.Join(dir, ".gitignore"), []byte("*.tmp\n.iterate/\n"), 0o644)
730732
cmd := exec.Command("git", "add", ".gitignore")
731733
cmd.Dir = dir
732734
cmd.CombinedOutput()

internal/evolution/phases.go

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,14 @@ func buildPlanPrompt(repoPath, journal, day, issues string) string {
6969
learnings, _ := os.ReadFile(filepath.Join(repoPath, "memory", "ACTIVE_LEARNINGS.md"))
7070
ciStatus, _ := os.ReadFile(filepath.Join(repoPath, ".iterate", "ci_status.txt"))
7171

72+
// Run codebase analysis for smarter task selection
73+
analysis := AnalyzeCodebase(repoPath)
74+
analysisStr := analysis.FormatAnalysis()
75+
7276
var sb strings.Builder
7377
appendPlanInstructions(&sb, ciStatus, day)
78+
sb.WriteString("## Codebase Analysis\n\n")
79+
sb.WriteString(analysisStr)
7480
appendPlanContext(&sb, learnings, journal, issues)
7581
return sb.String()
7682
}
@@ -165,9 +171,30 @@ func (e *Engine) RunImplementPhase(ctx context.Context, p iteragent.Provider) er
165171
return nil
166172
}
167173

168-
// executeTask runs a single task, reverts on failure.
174+
// executeTask runs a single task. On failure, reverts and retries once with error context.
169175
func (e *Engine) executeTask(ctx context.Context, p iteragent.Provider, task planTask, systemPrompt string, tools []iteragent.Tool, skills *iteragent.SkillSet, protectedWarning string) {
170-
userMsg := fmt.Sprintf("Implement Task %d: %s\n\n%s\n\nAfter implementing, run: go build ./... && go test ./...\nThen commit your changes.", task.Number, task.Description, protectedWarning)
176+
if ok := e.runTaskAttempt(ctx, p, task, systemPrompt, tools, skills, protectedWarning, ""); ok {
177+
return
178+
}
179+
180+
// First attempt failed. Retry with error context.
181+
e.logger.Info("retrying task after failure", "number", task.Number)
182+
v := e.verify(ctx)
183+
errorCtx := fmt.Sprintf("Previous attempt failed. Build passed: %v, Test passed: %v. Fix the errors and try again.", v.BuildPassed, v.TestPassed)
184+
if ok := e.runTaskAttempt(ctx, p, task, systemPrompt, tools, skills, protectedWarning, errorCtx); ok {
185+
e.logger.Info("task succeeded on retry", "number", task.Number)
186+
} else {
187+
e.logger.Warn("task failed after retry, skipping", "number", task.Number)
188+
}
189+
}
190+
191+
// runTaskAttempt executes one attempt at a task. Returns true on success.
192+
func (e *Engine) runTaskAttempt(ctx context.Context, p iteragent.Provider, task planTask, systemPrompt string, tools []iteragent.Tool, skills *iteragent.SkillSet, protectedWarning, extraContext string) bool {
193+
userMsg := fmt.Sprintf("Implement Task %d: %s\n\n%s", task.Number, task.Description, protectedWarning)
194+
if extraContext != "" {
195+
userMsg += "\n\n" + extraContext
196+
}
197+
userMsg += "\n\nAfter implementing, run: go build ./... && go test ./...\nThen commit your changes."
171198

172199
a := e.newAgent(p, tools, systemPrompt, skills)
173200
var taskOutput string
@@ -183,26 +210,26 @@ func (e *Engine) executeTask(ctx context.Context, p iteragent.Provider, task pla
183210
a.Finish()
184211

185212
if taskErr != nil {
186-
e.logger.Warn("task failed, reverting", "number", task.Number, "err", taskErr)
213+
e.logger.Warn("task error", "number", task.Number, "err", taskErr)
187214
_ = e.revert(ctx)
188-
return
215+
return false
189216
}
190217

191218
if violations, _ := e.verifyProtected(ctx); len(violations) > 0 {
192219
e.logger.Warn("protected files modified, reverting", "number", task.Number, "files", violations)
193220
_ = e.revert(ctx)
194-
return
221+
return false
195222
}
196223

197224
v := e.verify(ctx)
198225
if !v.BuildPassed || !v.TestPassed {
199226
e.logger.Warn("verification failed, reverting", "number", task.Number, "build", v.BuildPassed, "test", v.TestPassed)
200227
_ = e.revert(ctx)
201-
return
228+
return false
202229
}
203230

204231
_ = e.appendLearningJSONL(firstLine(extractCommitMessage(taskOutput)), "evolution", task.Description, "")
205-
_ = taskOutput
232+
return true
206233
}
207234

208235
// loadImplementContext prepares system prompt, tools, and skills for implementation.

internal/evolution/prompts.go

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,43 @@ func buildSystemPrompt(repoPath, identity string) string {
1818
## Personality
1919
%s
2020
21-
## Tool call format
21+
## Available Tools
2222
23-
To call a tool, output a fenced code block with language "tool":
23+
You have these tools. USE THEM. Do not just describe what you would do — actually do it.
2424
25+
### read_file — Read a file
2526
`+"```"+`tool
26-
{"tool":"tool_name","args":{"key":"value"}}
27+
{"tool":"read_file","args":{"path":"path/to/file.go"}}
2728
`+"```"+`
2829
29-
Examples:
30-
31-
Read a file:
30+
### write_file — Write/create a file
3231
`+"```"+`tool
33-
{"tool":"read_file","args":{"path":"internal/evolution/engine.go"}}
32+
{"tool":"write_file","args":{"path":"SESSION_PLAN.md","content":"## Session Plan\n\nSession Title: My task\n\n### Task 1: Do something\nFiles: cmd/foo.go\nDescription: Fix the thing\nIssue: none\n\n### Issue Responses\n"}}
3433
`+"```"+`
3534
36-
Write a file:
35+
### edit_file — Edit part of a file
3736
`+"```"+`tool
38-
{"tool":"write_file","args":{"path":"SESSION_PLAN.md","content":"## Session Plan\n\nSession Title: Fix nil pointer\n\n### Task 1: Fix nil check\nFiles: cmd/iterate/repl.go\nDescription: Add nil check on line 47\nIssue: none\n\n### Issue Responses\n"}}
37+
{"tool":"edit_file","args":{"path":"cmd/foo.go","old_string":"old code here","new_string":"new code here"}}
3938
`+"```"+`
4039
41-
Run a bash command:
40+
### bash — Run a shell command
4241
`+"```"+`tool
4342
{"tool":"bash","args":{"cmd":"go test ./..."}}
4443
`+"```"+`
4544
46-
CRITICAL: You MUST use this format to call tools. Do NOT just describe what you would do.`,
45+
### list_files — List files in a directory
46+
`+"```"+`tool
47+
{"tool":"list_files","args":{"path":"cmd/iterate"}}
48+
`+"```"+`
49+
50+
## Rules
51+
52+
1. ALWAYS use tools to read files before editing them
53+
2. After writing code, ALWAYS run: go build ./... && go test ./...
54+
3. If tests fail, fix the code and try again
55+
4. If you need to create SESSION_PLAN.md, use write_file
56+
5. Be direct. No explanations. Just act.
57+
6. One tool call at a time. Wait for results before next action.`,
4758
identity,
4859
string(personality),
4960
)
@@ -55,6 +66,7 @@ func buildUserMessage(repoPath, journal, issues string) string {
5566
var sb strings.Builder
5667
sb.WriteString("## Your task\n\n")
5768
sb.WriteString("Assess your codebase, find one meaningful improvement, implement it, test it, and commit it.\n\n")
69+
sb.WriteString("Start by listing files with list_files, then read the source code.\n\n")
5870

5971
if len(learnings) > 0 {
6072
l := string(learnings)
@@ -79,6 +91,6 @@ func buildUserMessage(repoPath, journal, issues string) string {
7991
sb.WriteString(issues + "\n")
8092
}
8193

82-
sb.WriteString("Begin your self-assessment now.")
94+
sb.WriteString("Begin now. Use tools. Don't just describe — act.")
8395
return sb.String()
8496
}

0 commit comments

Comments
 (0)