Skip to content

Commit f6fefe5

Browse files
committed
feat: implement 6-phase evolution pipeline (plan, implement, pr, review, merge, communicate)
Add RunPRPhase, RunReviewPhase, and RunMergePhase to the evolution engine, matching the 6 phases advertised on the website. Update evolve.sh to call all 6 phases sequentially, removing the duplicate shell-based PR/review/merge block that previously duplicated logic from the engine.
1 parent 9e13390 commit f6fefe5

3 files changed

Lines changed: 213 additions & 163 deletions

File tree

cmd/iterate/main_mode.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,30 @@ func runEvolutionPhase(ctx context.Context, f mainFlags, p iteragent.Provider, e
148148
}
149149
logger.Info("implement phase complete")
150150
return nil
151+
case "pr":
152+
logger.Info("running PR phase")
153+
if err := engine.RunPRPhase(ctx); err != nil {
154+
logger.Error("PR phase failed", "err", err)
155+
os.Exit(1)
156+
}
157+
logger.Info("PR phase complete")
158+
return nil
159+
case "review":
160+
logger.Info("running review phase")
161+
if err := engine.RunReviewPhase(ctx, p); err != nil {
162+
logger.Error("review phase failed", "err", err)
163+
os.Exit(1)
164+
}
165+
logger.Info("review phase complete")
166+
return nil
167+
case "merge":
168+
logger.Info("running merge phase")
169+
if err := engine.RunMergePhase(ctx); err != nil {
170+
logger.Error("merge phase failed", "err", err)
171+
os.Exit(1)
172+
}
173+
logger.Info("merge phase complete")
174+
return nil
151175
case "communicate":
152176
logger.Info("running communicate phase")
153177
if err := engine.RunCommunicatePhase(ctx, p); err != nil {

internal/evolution/phases_pr.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package evolution
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"path/filepath"
8+
"strings"
9+
10+
iteragent "github.com/GrayCodeAI/iteragent"
11+
)
12+
13+
// RunPRPhase creates a feature branch from the current HEAD, pushes it, and opens a PR.
14+
// It is designed to run after RunImplementPhase has already committed changes to main.
15+
func (e *Engine) RunPRPhase(ctx context.Context) error {
16+
ctx, cancel := withTimeout(ctx)
17+
defer cancel()
18+
19+
day := e.readDayCount()
20+
21+
// Sync with remote before branching.
22+
if out, err := e.runTool(ctx, "bash", map[string]string{
23+
"cmd": "git pull --rebase origin main",
24+
}); err != nil {
25+
e.logger.Warn("pull rebase failed, continuing", "err", err, "output", out)
26+
}
27+
28+
// Skip if nothing has changed relative to origin/main.
29+
out, _ := e.runTool(ctx, "bash", map[string]string{
30+
"cmd": "git diff origin/main HEAD --stat",
31+
})
32+
if strings.TrimSpace(out) == "" {
33+
e.logger.Info("no changes vs origin/main — skipping PR creation")
34+
return nil
35+
}
36+
37+
// Create feature branch at current HEAD (implement commits are already here).
38+
branchName := fmt.Sprintf("evolution/day-%s", day)
39+
e.branchName = branchName
40+
41+
// Delete any stale branch with the same name.
42+
_ = e.deleteBranch(ctx, branchName)
43+
44+
if out, err := e.runTool(ctx, "bash", map[string]string{
45+
"cmd": fmt.Sprintf("git checkout -b %s", branchName),
46+
}); err != nil {
47+
return fmt.Errorf("failed to create feature branch %s: %w (output: %s)", branchName, err, out)
48+
}
49+
e.logger.Info("created feature branch", "branch", branchName)
50+
51+
// Push with lease; fall back to a plain push on failure.
52+
if out, err := e.runTool(ctx, "bash", map[string]string{
53+
"cmd": fmt.Sprintf("git push -u origin %q --force-with-lease", branchName),
54+
}); err != nil {
55+
e.logger.Warn("push with lease failed, retrying", "err", err, "output", out)
56+
if out2, err2 := e.runTool(ctx, "bash", map[string]string{
57+
"cmd": fmt.Sprintf("git push -u origin %q", branchName),
58+
}); err2 != nil {
59+
return fmt.Errorf("failed to push branch: %w (output: %s)", err2, out2)
60+
}
61+
}
62+
e.logger.Info("pushed branch", "branch", branchName)
63+
64+
// Build PR title/body from the session plan.
65+
planBytes, _ := os.ReadFile(filepath.Join(e.repoPath, "SESSION_PLAN.md"))
66+
plan := string(planBytes)
67+
issueNums := extractIssueNumbers(plan)
68+
69+
sessionTitle := extractSessionTitle(plan)
70+
prTitle := fmt.Sprintf("iterate: Day %s evolution session", day)
71+
if sessionTitle != "" {
72+
prTitle = sessionTitle
73+
}
74+
prBody := buildPRBody(plan, "")
75+
76+
prNum, prURL, err := e.createPR(ctx, prTitle, prBody, issueNums)
77+
if err != nil {
78+
return fmt.Errorf("PR creation failed: %w", err)
79+
}
80+
e.logger.Info("PR created", "number", prNum, "url", prURL)
81+
82+
return e.savePRState()
83+
}
84+
85+
// RunReviewPhase runs an AI self-review of the open PR.
86+
func (e *Engine) RunReviewPhase(ctx context.Context, p iteragent.Provider) error {
87+
ctx, cancel := withTimeout(ctx)
88+
defer cancel()
89+
90+
if e.prNumber == 0 {
91+
e.logger.Info("no PR to review — skipping review phase")
92+
return nil
93+
}
94+
95+
identity, err := os.ReadFile(filepath.Join(e.repoPath, "docs/IDENTITY.md"))
96+
if err != nil {
97+
e.logger.Warn("failed to load IDENTITY.md for review phase", "err", err)
98+
}
99+
systemPrompt := buildSystemPrompt(e.repoPath, string(identity))
100+
101+
if err := e.reviewPR(ctx, p, e.tools, systemPrompt, e.skills); err != nil {
102+
e.logger.Warn("PR review encountered issues", "err", err)
103+
}
104+
return nil
105+
}
106+
107+
// RunMergePhase merges the open PR, clears state, and returns to main.
108+
func (e *Engine) RunMergePhase(ctx context.Context) error {
109+
ctx, cancel := withTimeout(ctx)
110+
defer cancel()
111+
112+
if e.prNumber == 0 {
113+
e.logger.Info("no PR to merge — skipping merge phase")
114+
return nil
115+
}
116+
117+
if err := e.mergePR(ctx); err != nil {
118+
return fmt.Errorf("merge failed: %w", err)
119+
}
120+
e.logger.Info("PR merged", "number", e.prNumber)
121+
122+
e.clearPRState()
123+
124+
if err := e.switchToMain(ctx); err != nil {
125+
e.logger.Warn("failed to switch to main after merge", "err", err)
126+
}
127+
128+
// Pull to make sure local main is up-to-date.
129+
if out, err := e.runTool(ctx, "bash", map[string]string{
130+
"cmd": "git pull origin main",
131+
}); err != nil {
132+
e.logger.Warn("pull after merge failed", "err", err, "output", out)
133+
}
134+
135+
return nil
136+
}

0 commit comments

Comments
 (0)