@@ -16,6 +16,12 @@ pub const ExecutionResult = enum {
1616 failure ,
1717};
1818
19+ /// Result of idea selection containing index and reason
20+ pub const IdeaSelection = struct {
21+ index : usize ,
22+ reason : []const u8 ,
23+ };
24+
1925/// Executor for running opencode CLI commands
2026pub const Executor = struct {
2127 cfg : * const config.Config ,
@@ -133,6 +139,77 @@ pub const Executor = struct {
133139 return "NEEDS_WORK" ;
134140 }
135141
142+ /// Run idea selection - AI picks simplest idea considering dependencies
143+ /// Returns IdeaSelection with 0-indexed idea number and reason, or null if parsing fails
144+ pub fn runIdeaSelection (self : * Executor , ideas_formatted : []const u8 , cycle : u32 ) ! ? IdeaSelection {
145+ self .log .statusFmt ("[Cycle {d}] Selecting idea..." , .{cycle });
146+
147+ const prompt = try plan .generateIdeaSelectionPrompt (ideas_formatted , self .allocator );
148+ defer self .allocator .free (prompt );
149+
150+ var title_buf : [64 ]u8 = undefined ;
151+ const title = std .fmt .bufPrint (& title_buf , "Opencoder Idea Selection Cycle {d}" , .{cycle }) catch "Opencoder Idea Selection" ;
152+
153+ // Run and capture output
154+ const output = self .runOpencode (self .cfg .planning_model , title , prompt ) catch | err | {
155+ self .log .logErrorFmt ("[Cycle {d}] Idea selection failed: {s}" , .{ cycle , @errorName (err ) });
156+ return null ;
157+ };
158+ defer self .allocator .free (output );
159+
160+ // Parse "SELECTED_IDEA: <number>" from output
161+ var selected_index : ? usize = null ;
162+ if (std .mem .indexOf (u8 , output , "SELECTED_IDEA:" )) | start | {
163+ const after_colon = output [start + 14 .. ]; // Skip "SELECTED_IDEA:"
164+ const trimmed = std .mem .trim (u8 , after_colon , " \t \n \r " );
165+
166+ // Parse the first number found
167+ var end : usize = 0 ;
168+ while (end < trimmed .len and trimmed [end ] >= '0' and trimmed [end ] <= '9' ) : (end += 1 ) {}
169+
170+ if (end > 0 ) {
171+ const num = std .fmt .parseInt (usize , trimmed [0.. end ], 10 ) catch return null ;
172+ if (num >= 1 ) {
173+ selected_index = num - 1 ; // Convert to 0-indexed
174+ }
175+ }
176+ }
177+
178+ if (selected_index == null ) {
179+ self .log .logError ("Failed to parse SELECTED_IDEA from AI response" );
180+ return null ;
181+ }
182+
183+ // Parse "REASON: <text>" from output
184+ var reason : []const u8 = "No reason provided" ;
185+ if (std .mem .indexOf (u8 , output , "REASON:" )) | start | {
186+ const after_colon = output [start + 7 .. ]; // Skip "REASON:"
187+ const trimmed = std .mem .trim (u8 , after_colon , " \t " );
188+
189+ // Find end of line or end of string
190+ const end = std .mem .indexOf (u8 , trimmed , "\n " ) orelse trimmed .len ;
191+ reason = std .mem .trim (u8 , trimmed [0.. end ], " \t \n \r " );
192+ }
193+
194+ return IdeaSelection {
195+ .index = selected_index .? ,
196+ .reason = try self .allocator .dupe (u8 , reason ),
197+ };
198+ }
199+
200+ /// Run planning phase for a specific idea
201+ pub fn runIdeaPlanning (self : * Executor , idea_content : []const u8 , idea_filename : []const u8 , cycle : u32 ) ! ExecutionResult {
202+ self .log .statusFmt ("[Cycle {d}] Planning for idea: {s}" , .{ cycle , idea_filename });
203+
204+ const prompt = try plan .generateIdeaPlanningPrompt (cycle , idea_content , idea_filename , self .allocator );
205+ defer self .allocator .free (prompt );
206+
207+ var title_buf : [64 ]u8 = undefined ;
208+ const title = std .fmt .bufPrint (& title_buf , "Opencoder Planning Cycle {d}" , .{cycle }) catch "Opencoder Planning" ;
209+
210+ return try self .runWithRetry (self .cfg .planning_model , title , prompt );
211+ }
212+
136213 /// Kill current child process if running
137214 pub fn killCurrentChild (self : * Executor ) void {
138215 if (self .current_child_pid ) | pid | {
0 commit comments