@@ -17,12 +17,8 @@ import {
1717 readFileOrNull ,
1818 writeFile ,
1919} from "./fs.ts"
20- import {
21- formatIdeasForSelection ,
22- loadAllIdeas ,
23- parseIdeaSelection ,
24- removeIdeaByIndex ,
25- } from "./ideas.ts"
20+ import { commitChanges , generateCommitMessage , hasChanges , pushChanges } from "./git.ts"
21+ import { formatIdeasForSelection , loadAllIdeas , parseIdeaSelection , removeIdea } from "./ideas.ts"
2622import { Logger } from "./logger.ts"
2723import { getTasks , getUncompletedTasks , markTaskComplete , validatePlan } from "./plan.ts"
2824import { loadState , saveState } from "./state.ts"
@@ -88,7 +84,7 @@ export async function runLoop(config: Config): Promise<void> {
8884 break
8985
9086 case "evaluation" :
91- await runEvaluationPhase ( state , builder , paths , logger )
87+ await runEvaluationPhase ( state , builder , paths , logger , config )
9288 break
9389 }
9490 } catch ( err ) {
@@ -117,9 +113,12 @@ export async function runLoop(config: Config): Promise<void> {
117113}
118114
119115/**
120- * Log startup information
116+ * Log startup information.
117+ * Exported for testing.
118+ * @param logger - Logger instance
119+ * @param config - Application configuration
121120 */
122- function logStartupInfo ( logger : Logger , config : Config ) : void {
121+ export function logStartupInfo ( logger : Logger , config : Config ) : void {
123122 // Get version from build-time define or package.json
124123 const version = typeof VERSION !== "undefined" ? VERSION : "1.0.0"
125124
@@ -136,10 +135,12 @@ function logStartupInfo(logger: Logger, config: Config): void {
136135}
137136
138137/**
139- * Setup signal handlers for graceful shutdown
138+ * Setup signal handlers for graceful shutdown.
139+ * @param logger - Logger instance for output
140+ * @param _builder - Builder instance (unused but kept for potential future use)
140141 */
141142function setupSignalHandlers ( logger : Logger , _builder : Builder ) : void {
142- const handleShutdown = async ( signal : string ) => {
143+ const handleShutdown = async ( signal : string ) : Promise < void > => {
143144 if ( forceShutdown ) {
144145 logger . say ( "\nForce quit!" )
145146 process . exit ( 130 )
@@ -156,8 +157,12 @@ function setupSignalHandlers(logger: Logger, _builder: Builder): void {
156157 logger . say ( "Press Ctrl+C again to force quit." )
157158 }
158159
159- process . on ( "SIGINT" , ( ) => handleShutdown ( "SIGINT" ) )
160- process . on ( "SIGTERM" , ( ) => handleShutdown ( "SIGTERM" ) )
160+ process . on ( "SIGINT" , ( ) : void => {
161+ handleShutdown ( "SIGINT" )
162+ } )
163+ process . on ( "SIGTERM" , ( ) : void => {
164+ handleShutdown ( "SIGTERM" )
165+ } )
161166}
162167
163168/**
@@ -170,58 +175,118 @@ async function runPlanPhase(
170175 logger : Logger ,
171176 config : Config ,
172177) : Promise < void > {
173- // Check for ideas first
174- const ideas = await loadAllIdeas ( paths . ideasDir )
175-
176178 let planContent : string
179+ let ideaToRemove : { path : string ; filename : string } | null = null
180+
181+ // Check if we're resuming with a previously selected idea
182+ if ( state . currentIdeaPath && state . currentIdeaFilename ) {
183+ logger . info ( `Resuming with idea: ${ state . currentIdeaFilename } ` )
184+
185+ // Try to read the idea content (it might still exist if we crashed before deleting)
186+ try {
187+ const content = await Bun . file ( state . currentIdeaPath ) . text ( )
188+ if ( content . trim ( ) ) {
189+ ideaToRemove = { path : state . currentIdeaPath , filename : state . currentIdeaFilename }
190+ planContent = await builder . runIdeaPlan ( content , state . currentIdeaFilename , state . cycle )
191+ } else {
192+ // Idea file is empty or gone, clear the state and proceed with normal planning
193+ logger . warn (
194+ `Idea file ${ state . currentIdeaFilename } is empty, proceeding with autonomous plan` ,
195+ )
196+ state . currentIdeaPath = undefined
197+ state . currentIdeaFilename = undefined
198+ planContent = await builder . runPlan ( state . cycle , config . userHint )
199+ }
200+ } catch {
201+ // Idea file doesn't exist anymore, but we have it in state - clear and proceed
202+ logger . warn (
203+ `Idea file ${ state . currentIdeaFilename } not found, proceeding with autonomous plan` ,
204+ )
205+ state . currentIdeaPath = undefined
206+ state . currentIdeaFilename = undefined
207+ planContent = await builder . runPlan ( state . cycle , config . userHint )
208+ }
209+ } else {
210+ // Check for new ideas
211+ const ideas = await loadAllIdeas ( paths . ideasDir )
177212
178- if ( ideas . length > 0 ) {
179- logger . info ( `Found ${ ideas . length } idea(s) in queue` )
213+ if ( ideas . length > 0 ) {
214+ logger . info ( `Found ${ ideas . length } idea(s) in queue` )
180215
181- if ( ideas . length === 1 ) {
182- // Single idea - use directly
183- const idea = ideas [ 0 ]
184- if ( ! idea ) throw new Error ( "Unexpected: ideas[0] is undefined" )
185- logger . say ( `Using idea: ${ idea . filename } ` )
186- removeIdeaByIndex ( ideas , 0 )
187- planContent = await builder . runIdeaPlan ( idea . content , idea . filename , state . cycle )
188- } else {
189- // Multiple ideas - let AI select
190- const formatted = formatIdeasForSelection ( ideas )
191- const selection = await builder . runIdeaSelection ( formatted , state . cycle )
192- const selectedIndex = parseIdeaSelection ( selection )
193-
194- if ( selectedIndex !== null && selectedIndex < ideas . length ) {
195- const idea = ideas [ selectedIndex ]
196- if ( ! idea ) throw new Error ( "Unexpected: selected idea is undefined" )
197- logger . success ( `AI selected idea: ${ idea . filename } ` )
198- removeIdeaByIndex ( ideas , selectedIndex )
199- planContent = await builder . runIdeaPlan ( idea . content , idea . filename , state . cycle )
216+ let selectedIdea : { path : string ; filename : string ; content : string } | null = null
217+
218+ if ( ideas . length === 1 ) {
219+ // Single idea - use directly
220+ const idea = ideas [ 0 ]
221+ if ( ! idea ) throw new Error ( "Unexpected: ideas[0] is undefined" )
222+ selectedIdea = idea
223+ logger . say ( `Using idea: ${ idea . filename } ` )
224+ } else {
225+ // Multiple ideas - let AI select
226+ const formatted = formatIdeasForSelection ( ideas )
227+ const selection = await builder . runIdeaSelection ( formatted , state . cycle )
228+ const selectedIndex = parseIdeaSelection ( selection )
229+
230+ if ( selectedIndex !== null && selectedIndex < ideas . length ) {
231+ const idea = ideas [ selectedIndex ]
232+ if ( ! idea ) throw new Error ( "Unexpected: selected idea is undefined" )
233+ selectedIdea = idea
234+ logger . success ( `AI selected idea: ${ idea . filename } ` )
235+ } else {
236+ // Fallback to autonomous plan
237+ logger . warn ( "Could not parse idea selection, falling back to autonomous plan" )
238+ }
239+ }
240+
241+ if ( selectedIdea ) {
242+ // Save the selected idea to state BEFORE creating the plan
243+ // This ensures we can resume if the process crashes
244+ state . currentIdeaPath = selectedIdea . path
245+ state . currentIdeaFilename = selectedIdea . filename
246+ await saveState ( paths . stateFile , state )
247+
248+ ideaToRemove = { path : selectedIdea . path , filename : selectedIdea . filename }
249+ planContent = await builder . runIdeaPlan (
250+ selectedIdea . content ,
251+ selectedIdea . filename ,
252+ state . cycle ,
253+ )
200254 } else {
201- // Fallback to autonomous plan
202- logger . warn ( "Could not parse idea selection, falling back to autonomous plan" )
255+ // No valid idea selected, fall back to autonomous plan
203256 planContent = await builder . runPlan ( state . cycle , config . userHint )
204257 }
258+ } else {
259+ // No ideas - autonomous plan
260+ planContent = await builder . runPlan ( state . cycle , config . userHint )
205261 }
206- } else {
207- // No ideas - autonomous plan
208- planContent = await builder . runPlan ( state . cycle , config . userHint )
209262 }
210263
211264 // Validate the plan
212265 const validation = validatePlan ( planContent )
213266 if ( ! validation . valid ) {
214267 logger . logError ( `Invalid plan: ${ validation . error } ` )
215- // Stay in plan phase to retry
268+ // Stay in plan phase to retry - don't remove the idea yet
216269 return
217270 }
218271
219- // Save the plan
272+ // Save the plan FIRST
220273 await writeFile ( paths . currentPlan , planContent )
221274
222275 const tasks = getTasks ( planContent )
223276 logger . success ( `Plan created with ${ tasks . length } tasks` )
224277
278+ // Only NOW remove the idea file, after plan is safely saved
279+ if ( ideaToRemove ) {
280+ const removed = removeIdea ( ideaToRemove . path )
281+ if ( removed ) {
282+ logger . logVerbose ( `Removed processed idea: ${ ideaToRemove . filename } ` )
283+ }
284+ }
285+
286+ // Clear the idea from state since it's now processed
287+ state . currentIdeaPath = undefined
288+ state . currentIdeaFilename = undefined
289+
225290 // Transition to build
226291 state . phase = "build"
227292 state . taskIndex = 0
@@ -277,7 +342,7 @@ async function runBuildPhase(
277342 if ( shutdownRequested ) return
278343
279344 // Build the task
280- logger . activity ( " Building task" , ` ${ state . currentTaskNum } /${ tasks . length } `)
345+ logger . info ( ` Building task ${ state . currentTaskNum } /${ tasks . length } `)
281346 const result = await builder . runTask (
282347 nextTask . description ,
283348 state . cycle ,
@@ -291,6 +356,12 @@ async function runBuildPhase(
291356 await writeFile ( paths . currentPlan , updatedPlan )
292357
293358 logger . success ( `Task ${ state . currentTaskNum } /${ tasks . length } complete` )
359+
360+ // Auto-commit changes if enabled
361+ if ( config . autoCommit && hasChanges ( config . projectDir ) ) {
362+ const commitMessage = generateCommitMessage ( nextTask . description )
363+ commitChanges ( config . projectDir , logger , commitMessage , config . commitSignoff )
364+ }
294365 } else {
295366 logger . logError ( `Task failed: ${ result . error } ` )
296367 // Continue to next task or retry logic could go here
@@ -310,6 +381,7 @@ async function runEvaluationPhase(
310381 builder : Builder ,
311382 paths : Paths ,
312383 logger : Logger ,
384+ config : Config ,
313385) : Promise < void > {
314386 // Read current plan for evaluation
315387 const planContent = await readFileOrNull ( paths . currentPlan )
@@ -333,13 +405,20 @@ async function runEvaluationPhase(
333405 // Archive the completed plan
334406 await archivePlan ( paths , state . cycle , logger )
335407
408+ // Auto-push commits if enabled
409+ if ( config . autoPush && hasChanges ( config . projectDir ) ) {
410+ pushChanges ( config . projectDir , logger )
411+ }
412+
336413 // Start new cycle
337414 state . cycle ++
338415 state . phase = "plan"
339416 state . taskIndex = 0
340417 state . totalTasks = 0
341418 state . currentTaskNum = 0
342419 state . currentTaskDesc = ""
420+ state . currentIdeaPath = undefined
421+ state . currentIdeaFilename = undefined
343422
344423 // Clear session for new cycle
345424 builder . clearSession ( )
@@ -353,9 +432,13 @@ async function runEvaluationPhase(
353432}
354433
355434/**
356- * Archive the completed plan to history
435+ * Archive the completed plan to history.
436+ * Exported for testing.
437+ * @param paths - Workspace paths
438+ * @param cycle - Current cycle number
439+ * @param logger - Logger instance
357440 */
358- async function archivePlan ( paths : Paths , cycle : number , logger : Logger ) : Promise < void > {
441+ export async function archivePlan ( paths : Paths , cycle : number , logger : Logger ) : Promise < void > {
359442 const planContent = await readFileOrNull ( paths . currentPlan )
360443 if ( ! planContent ) return
361444
@@ -368,18 +451,38 @@ async function archivePlan(paths: Paths, cycle: number, logger: Logger): Promise
368451}
369452
370453/**
371- * Sleep for a given number of milliseconds
454+ * Sleep for a given number of milliseconds.
455+ * Exported for testing and reuse.
456+ * @param ms - Number of milliseconds to sleep
457+ * @returns Promise that resolves after the specified time
372458 */
373- function sleep ( ms : number ) : Promise < void > {
459+ export function sleep ( ms : number ) : Promise < void > {
374460 return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) )
375461}
376462
377463/**
378- * Check if shutdown has been requested
464+ * Check if shutdown has been requested.
465+ * Exported for testing and external shutdown checks.
379466 */
380467export function isShutdownRequested ( ) : boolean {
381468 return shutdownRequested
382469}
383470
471+ /**
472+ * Reset shutdown flags. Only for testing purposes.
473+ * WARNING: Do not use in production code.
474+ */
475+ export function resetShutdownFlags ( ) : void {
476+ shutdownRequested = false
477+ forceShutdown = false
478+ }
479+
480+ /**
481+ * Request a shutdown. Used for programmatic shutdown triggering.
482+ */
483+ export function requestShutdown ( ) : void {
484+ shutdownRequested = true
485+ }
486+
384487// Declare VERSION as a global that will be defined at build time
385488declare const VERSION : string
0 commit comments