Skip to content

Commit e11cc73

Browse files
committed
test message
1 parent 1230645 commit e11cc73

8 files changed

Lines changed: 134 additions & 91 deletions

File tree

cmd/iterate/repl.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,11 +224,13 @@ func handleModelProviderSwitch(line string, p *iteragent.Provider, thinking *ite
224224
_ = (*a).Close() // best-effort cleanup
225225
*a = makeAgent(*p, repoPath, *thinking, logger)
226226
fmt.Printf("%s✓ switched to %s%s\n\n", colorLime, (*p).Name(), colorReset)
227-
saveConfig(iterConfig{
228-
Provider: os.Getenv("ITERATE_PROVIDER"),
229-
Model: os.Getenv("ITERATE_MODEL"),
230-
OllamaBaseURL: os.Getenv("OLLAMA_BASE_URL"),
231-
})
227+
// Preserve all existing config — only update provider/model fields.
228+
updatedCfg := loadConfig()
229+
updatedCfg.Provider = (*p).Name()
230+
if model := os.Getenv("ITERATE_MODEL"); model != "" {
231+
updatedCfg.Model = model
232+
}
233+
saveConfig(updatedCfg)
232234
}
233235
return true
234236
}

internal/agent/pool.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ type Pool struct {
2020
rateLimiter *RateLimiter
2121
logger *slog.Logger
2222
maxAgents int
23-
wg sync.WaitGroup
2423
}
2524

2625
// RateLimiter controls API call frequency using token bucket algorithm.
2726
type RateLimiter struct {
2827
tokens chan struct{}
2928
refill time.Duration
3029
stopChan chan struct{}
30+
stopOnce sync.Once
3131
}
3232

3333
// NewRateLimiter creates a rate limiter with the given requests per second.
@@ -70,9 +70,11 @@ func (rl *RateLimiter) Wait(ctx context.Context) error {
7070
}
7171
}
7272

73-
// Stop stops the rate limiter goroutine.
73+
// Stop stops the rate limiter goroutine. Safe to call multiple times.
7474
func (rl *RateLimiter) Stop() {
75-
close(rl.stopChan)
75+
rl.stopOnce.Do(func() {
76+
close(rl.stopChan)
77+
})
7678
}
7779

7880
// NewPool creates an agent pool with rate limiting.
@@ -142,10 +144,6 @@ func (p *Pool) Spawn(ctx context.Context, task string, handler func(*iteragent.A
142144
return err
143145
}
144146
defer p.Release(agent)
145-
146-
p.wg.Add(1)
147-
defer p.wg.Done()
148-
149147
return handler(agent)
150148
}
151149

@@ -154,10 +152,13 @@ func (p *Pool) Spawn(ctx context.Context, task string, handler func(*iteragent.A
154152
func (p *Pool) SpawnAll(ctx context.Context, tasks []string, handler func(*iteragent.Agent, string) error) []error {
155153
errs := make([]error, len(tasks))
156154
var errMu sync.Mutex
155+
var wg sync.WaitGroup
157156

158157
for i, task := range tasks {
159158
i, task := i, task // capture loop variables
159+
wg.Add(1) // must be before goroutine launch to avoid Wait() returning early
160160
go func() {
161+
defer wg.Done()
161162
err := p.Spawn(ctx, task, func(agent *iteragent.Agent) error {
162163
return handler(agent, task)
163164
})
@@ -167,7 +168,7 @@ func (p *Pool) SpawnAll(ctx context.Context, tasks []string, handler func(*itera
167168
}()
168169
}
169170

170-
p.wg.Wait()
171+
wg.Wait()
171172
return errs
172173
}
173174

internal/community/github.go

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"log/slog"
77
"os"
88
"sort"
9+
"sync"
910

1011
"github.com/google/go-github/v61/github"
1112
"golang.org/x/oauth2"
@@ -73,37 +74,57 @@ func FetchIssues(ctx context.Context, owner, repo string, issueTypes []IssueType
7374
}
7475

7576
// buildIssuesFromGitHub converts GitHub API issues to Issue structs with reaction scores.
77+
// Reaction fetches are fanned out concurrently to avoid N+1 serial API calls.
7678
func buildIssuesFromGitHub(ctx context.Context, client *github.Client, owner, repo string, ghIssues []*github.Issue, issueType IssueType) []Issue {
77-
var issues []Issue
78-
for _, gi := range ghIssues {
79-
reactions, _, err := client.Reactions.ListIssueReactions(ctx, owner, repo, gi.GetNumber(), nil)
80-
if err != nil {
81-
continue
82-
}
83-
84-
var up, down int
85-
for _, r := range reactions {
86-
switch r.GetContent() {
87-
case "heart", "+1":
88-
up++
89-
case "-1":
90-
down++
79+
type slot struct {
80+
issue Issue
81+
ok bool
82+
}
83+
slots := make([]slot, len(ghIssues))
84+
var wg sync.WaitGroup
85+
86+
for i, gi := range ghIssues {
87+
i, gi := i, gi
88+
wg.Add(1)
89+
go func() {
90+
defer wg.Done()
91+
reactions, _, err := client.Reactions.ListIssueReactions(ctx, owner, repo, gi.GetNumber(), nil)
92+
if err != nil {
93+
return
9194
}
92-
}
95+
var up, down int
96+
for _, r := range reactions {
97+
switch r.GetContent() {
98+
case "heart", "+1":
99+
up++
100+
case "-1":
101+
down++
102+
}
103+
}
104+
body := gi.GetBody()
105+
if len(body) > 500 {
106+
body = body[:500] + "..."
107+
}
108+
slots[i] = slot{
109+
issue: Issue{
110+
Number: gi.GetNumber(),
111+
Title: gi.GetTitle(),
112+
Body: body,
113+
NetVotes: up - down,
114+
URL: gi.GetHTMLURL(),
115+
Type: issueType,
116+
},
117+
ok: true,
118+
}
119+
}()
120+
}
121+
wg.Wait()
93122

94-
body := gi.GetBody()
95-
if len(body) > 500 {
96-
body = body[:500] + "..."
123+
var issues []Issue
124+
for _, s := range slots {
125+
if s.ok {
126+
issues = append(issues, s.issue)
97127
}
98-
99-
issues = append(issues, Issue{
100-
Number: gi.GetNumber(),
101-
Title: gi.GetTitle(),
102-
Body: body,
103-
NetVotes: up - down,
104-
URL: gi.GetHTMLURL(),
105-
Type: issueType,
106-
})
107128
}
108129
return issues
109130
}

internal/evolution/engine.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,23 +34,20 @@ var ProtectedFiles = []string{
3434
"scripts/social/social.sh",
3535
}
3636

37-
// isProtected checks if a file path matches any protected pattern.
37+
// isProtected checks if a relative file path matches any protected pattern.
38+
// path must already be relative to the repo root.
3839
func isProtected(path string) bool {
3940
cleanPath := filepath.Clean(path)
4041
for _, pattern := range ProtectedFiles {
4142
// Check exact match
4243
if cleanPath == filepath.Clean(pattern) {
4344
return true
4445
}
45-
// Check glob pattern match
46-
if matched, _ := filepath.Match(pattern, filepath.Base(cleanPath)); matched {
47-
dir := filepath.Dir(cleanPath)
48-
patternDir := filepath.Dir(pattern)
49-
if dir == patternDir || patternDir == "." {
50-
return true
51-
}
46+
// Check glob pattern match against the full relative path
47+
if matched, _ := filepath.Match(pattern, cleanPath); matched {
48+
return true
5249
}
53-
// Check if path is inside a protected directory
50+
// Check if path is inside a protected directory (handles /*.go and /* suffixes)
5451
if strings.HasSuffix(pattern, "/*") || strings.HasSuffix(pattern, "/*.go") {
5552
protectedDir := strings.TrimSuffix(pattern, "/*")
5653
protectedDir = strings.TrimSuffix(protectedDir, "/*.go")
@@ -73,6 +70,7 @@ type Engine struct {
7370
prURL string
7471
branchName string
7572
traceID string
73+
toolMap map[string]iteragent.Tool // cached at construction to avoid re-init per call
7674
}
7775

7876
// generateTraceID creates a random hex trace ID for request correlation.
@@ -107,11 +105,13 @@ func New(repoPath string, logger *slog.Logger) *Engine {
107105
repo = "GrayCodeAI/iterate"
108106
}
109107
tid := generateTraceID()
108+
tools := iteragent.DefaultTools(repoPath)
110109
e := &Engine{
111110
repoPath: repoPath,
112111
repo: repo,
113112
logger: logger.With("traceID", tid),
114113
traceID: tid,
114+
toolMap: iteragent.ToolMap(tools),
115115
}
116116
// Load PR state from previous phase if exists
117117
e.loadPRState()
@@ -177,10 +177,9 @@ func (e *Engine) WithThinking(level iteragent.ThinkingLevel) *Engine {
177177
// Run executes one full evolution session.
178178
func (e *Engine) handlePostRunTests(ctx context.Context, day int, output string, p iteragent.Provider, tools []iteragent.Tool, skills *iteragent.SkillSet, result *RunResult) error {
179179
testResult, testErr := e.runTests(ctx)
180-
_ = testResult
181180

182181
if testErr != nil {
183-
e.logger.Info("tests failed, reverting changes")
182+
e.logger.Info("tests failed, reverting changes", "output", testResult)
184183
result.Status = "reverted"
185184
_ = e.revert(ctx)
186185
return nil
@@ -259,7 +258,7 @@ func (e *Engine) readContextFiles() ([]byte, []byte, int) {
259258
func (e *Engine) runAgentAndCollectEvents(ctx context.Context, a *iteragent.Agent, userMessage string) (string, error) {
260259
eventCh := a.Prompt(ctx, userMessage)
261260
var output string
262-
var runErr error
261+
var errs []string
263262
for ev := range eventCh {
264263
if e.eventSink != nil {
265264
select {
@@ -271,10 +270,13 @@ func (e *Engine) runAgentAndCollectEvents(ctx context.Context, a *iteragent.Agen
271270
output = ev.Content
272271
}
273272
if ev.Type == string(iteragent.EventError) {
274-
runErr = fmt.Errorf("%s", ev.Content)
273+
errs = append(errs, ev.Content)
275274
}
276275
}
277-
return output, runErr
276+
if len(errs) > 0 {
277+
return output, fmt.Errorf("%s", strings.Join(errs, "; "))
278+
}
279+
return output, nil
278280
}
279281

280282
// handleCommitAndPR creates a branch, commits, pushes, and creates a PR.

internal/evolution/git.go

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ import (
1313
)
1414

1515
func (e *Engine) runTool(ctx context.Context, name string, args map[string]string) (string, error) {
16-
tools := iteragent.DefaultTools(e.repoPath)
17-
tm := iteragent.ToolMap(tools)
18-
tool, ok := tm[name]
16+
tool, ok := e.toolMap[name]
1917
if !ok {
2018
return "", fmt.Errorf("tool %q not found", name)
2119
}
@@ -127,10 +125,10 @@ func (e *Engine) createPR(ctx context.Context, title, body string, issueNums []i
127125

128126
url := strings.TrimSpace(out)
129127
var prNum int
130-
fmt.Sscanf(url, "%*s/%d", &prNum)
131128
if idx := strings.LastIndex(url, "/"); idx >= 0 {
132-
numStr := url[idx+1:]
133-
fmt.Sscanf(numStr, "%d", &prNum)
129+
if _, err := fmt.Sscanf(url[idx+1:], "%d", &prNum); err != nil {
130+
e.logger.Warn("could not parse PR number from URL", "url", url)
131+
}
134132
}
135133

136134
e.prURL = url
@@ -208,7 +206,7 @@ func (e *Engine) mergePR(ctx context.Context) error {
208206
})
209207
if err != nil {
210208
if strings.Contains(strings.ToLower(out), "no mergeable") || strings.Contains(strings.ToLower(out), "conflict") {
211-
e.logger.Warn("PR has merge conflicts, attempting auto-merge")
209+
e.logger.Warn("PR has merge conflicts, attempting merge with --admin flag (bypasses branch protection)", "pr", e.prNumber)
212210
mergeOut, mergeErr := e.runTool(ctx, "bash", map[string]string{
213211
"cmd": fmt.Sprintf("gh pr merge %d --repo %s --squash --admin --delete-branch 2>&1 || echo 'MERGE_FAILED'", e.prNumber, e.repo),
214212
})
@@ -254,9 +252,7 @@ func (e *Engine) forwardEvents(src <-chan iteragent.Event) {
254252
}
255253

256254
func (e *Engine) runTests(ctx context.Context) (string, error) {
257-
tools := iteragent.DefaultTools(e.repoPath)
258-
tm := iteragent.ToolMap(tools)
259-
return tm["run_tests"].Execute(ctx, nil)
255+
return e.toolMap["run_tests"].Execute(ctx, nil)
260256
}
261257

262258
// defaultPhaseTimeout is the maximum duration for any evolution phase.
@@ -268,25 +264,19 @@ func withTimeout(ctx context.Context) (context.Context, context.CancelFunc) {
268264
}
269265

270266
func (e *Engine) revert(ctx context.Context) error {
271-
tools := iteragent.DefaultTools(e.repoPath)
272-
tm := iteragent.ToolMap(tools)
273-
274-
// First, reset all staged and unstaged changes
275-
_, err := e.runTool(ctx, "bash", map[string]string{
267+
// Reset uncommitted changes first.
268+
if _, err := e.runTool(ctx, "bash", map[string]string{
276269
"cmd": "git checkout -- . && git clean -fd",
277-
})
278-
if err != nil {
279-
e.logger.Warn("git checkout failed, trying revert tool", "err", err)
270+
}); err == nil {
271+
return nil
280272
}
281-
282-
// Then use the revert tool for any committed changes
283-
_, err = tm["git_revert"].Execute(ctx, nil)
273+
// Fall back: use git_revert tool for any committed changes.
274+
e.logger.Warn("git checkout failed, trying revert tool")
275+
_, err := e.toolMap["git_revert"].Execute(ctx, nil)
284276
return err
285277
}
286278

287279
func (e *Engine) commit(ctx context.Context, msg string) error {
288-
tools := iteragent.DefaultTools(e.repoPath)
289-
tm := iteragent.ToolMap(tools)
290-
_, err := tm["git_commit"].Execute(ctx, map[string]string{"message": msg})
280+
_, err := e.toolMap["git_commit"].Execute(ctx, map[string]string{"message": msg})
291281
return err
292282
}

0 commit comments

Comments
 (0)