@@ -2123,6 +2123,9 @@ fn build_story_executor(
21232123 skills_block : String ,
21242124 selected_skill_matches : Vec < crate :: services:: skills:: model:: SkillMatch > ,
21252125 knowledge_tool_params : Option < KnowledgeToolParams > ,
2126+ file_change_tracker : Option <
2127+ Arc < std:: sync:: Mutex < crate :: services:: file_change_tracker:: FileChangeTracker > > ,
2128+ > ,
21262129) -> impl Fn ( StoryExecutionContext ) -> Pin < Box < dyn Future < Output = StoryExecutionOutcome > + Send > >
21272130 + Send
21282131 + Sync
@@ -2141,6 +2144,7 @@ fn build_story_executor(
21412144 let skills_block = skills_block. clone ( ) ;
21422145 let selected_skill_matches = selected_skill_matches. clone ( ) ;
21432146 let knowledge_tool_params = knowledge_tool_params. clone ( ) ;
2147+ let file_change_tracker = file_change_tracker. clone ( ) ;
21442148 Box :: pin ( async move {
21452149 eprintln ! (
21462150 "[INFO] Executing story '{}' (attempt {}) with agent '{}' in {} [mode: {:?}]" ,
@@ -2181,6 +2185,13 @@ fn build_story_executor(
21812185
21822186 // Build execution prompt from story context
21832187 let prompt = build_story_prompt ( & ctx, & knowledge_block, & memory_block, & skills_block) ;
2188+ let file_change_turn_index = file_change_tracker. as_ref ( ) . and_then ( |tracker| {
2189+ tracker. lock ( ) . ok ( ) . map ( |mut guard| {
2190+ let next = guard. turn_index ( ) . saturating_add ( 1 ) ;
2191+ guard. set_turn_index ( next) ;
2192+ next
2193+ } )
2194+ } ) ;
21842195
21852196 match mode {
21862197 StoryExecutionMode :: Cli => {
@@ -2190,6 +2201,8 @@ fn build_story_executor(
21902201 & prompt,
21912202 & ctx. project_path ,
21922203 ctx. cancel_token . clone ( ) ,
2204+ file_change_tracker. clone ( ) ,
2205+ file_change_turn_index,
21932206 )
21942207 . await
21952208 }
@@ -2210,6 +2223,8 @@ fn build_story_executor(
22102223 selected_skill_matches. as_slice ( ) ,
22112224 knowledge_tool_params. as_ref ( ) ,
22122225 ctx. cancel_token . clone ( ) ,
2226+ file_change_tracker. clone ( ) ,
2227+ file_change_turn_index,
22132228 )
22142229 . await
22152230 }
@@ -2228,6 +2243,10 @@ async fn execute_story_via_agent(
22282243 prompt : & str ,
22292244 project_path : & std:: path:: Path ,
22302245 cancel_token : tokio_util:: sync:: CancellationToken ,
2246+ file_change_tracker : Option <
2247+ Arc < std:: sync:: Mutex < crate :: services:: file_change_tracker:: FileChangeTracker > > ,
2248+ > ,
2249+ file_change_turn_index : Option < u32 > ,
22312250) -> StoryExecutionOutcome {
22322251 use tokio:: process:: Command ;
22332252
@@ -2266,6 +2285,11 @@ async fn execute_story_via_agent(
22662285 project_path. display( )
22672286 ) ;
22682287
2288+ let before_workspace_snapshot = file_change_tracker
2289+ . as_ref ( )
2290+ . and_then ( |tracker| tracker. lock ( ) . ok ( ) )
2291+ . and_then ( |tracker| tracker. capture_workspace_snapshot ( ) . ok ( ) ) ;
2292+
22692293 let mut process_builder = Command :: new ( & command) ;
22702294 process_builder
22712295 . args ( & args)
@@ -2288,7 +2312,7 @@ async fn execute_story_via_agent(
22882312
22892313 let mut wait_handle = tokio:: spawn ( async move { child. wait_with_output ( ) . await } ) ;
22902314
2291- tokio:: select! {
2315+ let outcome = tokio:: select! {
22922316 _ = cancel_token. cancelled( ) => {
22932317 wait_handle. abort( ) ;
22942318 eprintln!( "[INFO] Agent '{}' execution cancelled" , command) ;
@@ -2338,7 +2362,29 @@ async fn execute_story_via_agent(
23382362 }
23392363 }
23402364 }
2365+ } ;
2366+
2367+ if let ( Some ( tracker) , Some ( before_snapshot) ) = (
2368+ file_change_tracker. as_ref ( ) ,
2369+ before_workspace_snapshot. as_ref ( ) ,
2370+ ) {
2371+ if let Ok ( mut tracker_guard) = tracker. lock ( ) {
2372+ if let Ok ( after_snapshot) = tracker_guard. capture_workspace_snapshot ( ) {
2373+ let turn_index = file_change_turn_index
2374+ . unwrap_or_else ( || tracker_guard. turn_index ( ) ) ;
2375+ tracker_guard. record_workspace_delta_between_at (
2376+ turn_index,
2377+ & format ! ( "agent-{}" , uuid:: Uuid :: new_v4( ) ) ,
2378+ "Bash" ,
2379+ before_snapshot,
2380+ & after_snapshot,
2381+ & format ! ( "{} story execution" , command) ,
2382+ ) ;
2383+ }
2384+ }
23412385 }
2386+
2387+ outcome
23422388}
23432389
23442390/// Execute a story via the OrchestratorService using direct LLM API.
@@ -2361,6 +2407,10 @@ async fn execute_story_via_llm(
23612407 selected_skill_matches : & [ crate :: services:: skills:: model:: SkillMatch ] ,
23622408 knowledge_tool_params : Option < & KnowledgeToolParams > ,
23632409 cancel_token : tokio_util:: sync:: CancellationToken ,
2410+ file_change_tracker : Option <
2411+ Arc < std:: sync:: Mutex < crate :: services:: file_change_tracker:: FileChangeTracker > > ,
2412+ > ,
2413+ file_change_turn_index : Option < u32 > ,
23642414) -> StoryExecutionOutcome {
23652415 use crate :: services:: orchestrator:: { OrchestratorConfig , OrchestratorService } ;
23662416 use crate :: services:: streaming:: UnifiedStreamEvent ;
@@ -2424,6 +2474,13 @@ async fn execute_story_via_llm(
24242474 . with_search_provider ( & search_provider, search_api_key)
24252475 . with_permission_gate ( permission_gate. clone ( ) ) ;
24262476
2477+ if let Some ( ref tracker) = file_change_tracker {
2478+ orchestrator = orchestrator. with_file_change_tracker ( Arc :: clone ( tracker) ) ;
2479+ }
2480+ if let Some ( turn_index) = file_change_turn_index {
2481+ orchestrator = orchestrator. with_file_change_turn_index ( turn_index) ;
2482+ }
2483+
24272484 if !selected_skill_matches. is_empty ( ) {
24282485 let selected_skills =
24292486 std:: sync:: Arc :: new ( tokio:: sync:: RwLock :: new ( selected_skill_matches. to_vec ( ) ) ) ;
0 commit comments