diff --git a/crates/executors/src/logs/utils/entry_index.rs b/crates/executors/src/logs/utils/entry_index.rs index c8ea66cc..6ddcf6f3 100644 --- a/crates/executors/src/logs/utils/entry_index.rs +++ b/crates/executors/src/logs/utils/entry_index.rs @@ -69,6 +69,14 @@ impl Default for EntryIndexProvider { } } +#[cfg(test)] +impl EntryIndexProvider { + /// Test-only constructor for a fresh provider starting at 0 + pub fn test_new() -> Self { + Self::new() + } +} + #[cfg(test)] mod tests { use super::*; @@ -103,11 +111,3 @@ mod tests { assert_eq!(provider.current(), 2); } } - -#[cfg(test)] -impl EntryIndexProvider { - /// Test-only constructor for a fresh provider starting at 0 - pub fn test_new() -> Self { - Self::new() - } -} diff --git a/crates/local-deployment/src/container.rs b/crates/local-deployment/src/container.rs index 57e59857..3c4febbd 100644 --- a/crates/local-deployment/src/container.rs +++ b/crates/local-deployment/src/container.rs @@ -114,6 +114,15 @@ impl LocalContainerService { )) } + /// Finalize task execution by updating status to InReview and sending notifications + async fn finalize_task(db: &DBService, config: &Arc>, ctx: &ExecutionContext) { + if let Err(e) = Task::update_status(&db.pool, ctx.task.id, TaskStatus::InReview).await { + tracing::error!("Failed to update task status to InReview: {e}"); + } + let notify_cfg = config.read().await.notifications.clone(); + NotificationService::notify_execution_halted(notify_cfg, ctx).await; + } + /// Defensively check for externally deleted worktrees and mark them as deleted in the database async fn check_externally_deleted_worktrees(db: &DBService) -> Result<(), DeploymentError> { let active_attempts = TaskAttempt::find_by_worktree_deleted(&db.pool).await?; @@ -335,28 +344,52 @@ impl LocalContainerService { ExecutionProcessStatus::Completed ) && exit_code == Some(0) { - if let Err(e) = container.try_commit_changes(&ctx).await { - tracing::error!("Failed to commit changes after execution: {}", e); - } + // Commit changes (if any) and get feedback about whether changes were made + let changes_committed = match container.try_commit_changes(&ctx).await { + Ok(committed) => committed, + Err(e) => { + tracing::error!( + "Failed to commit changes after execution: {}", + e + ); + // Treat commit failures as if changes were made to be safe + true + } + }; - // If the process exited successfully, start the next action - if let Err(e) = container.try_start_next_action(&ctx).await { - tracing::error!( - "Failed to start next action after completion: {}", - e + // Determine whether to start the next action based on execution context + let should_start_next = if matches!( + ctx.execution_process.run_reason, + ExecutionProcessRunReason::CodingAgent + ) { + // Skip CleanupScript when CodingAgent produced no changes + changes_committed + } else { + // SetupScript always proceeds to CodingAgent + true + }; + + if should_start_next { + // If the process exited successfully, start the next action + if let Err(e) = container.try_start_next_action(&ctx).await { + tracing::error!( + "Failed to start next action after completion: {}", + e + ); + } + } else { + tracing::info!( + "Skipping cleanup script for task attempt {} - no changes made by coding agent", + ctx.task_attempt.id ); + + // Manually finalize task since we're bypassing normal execution flow + Self::finalize_task(&db, &config, &ctx).await; } } if Self::should_finalize(&ctx) { - if let Err(e) = - Task::update_status(&db.pool, ctx.task.id, TaskStatus::InReview) - .await - { - tracing::error!("Failed to update task status to InReview: {e}"); - } - let notify_cfg = config.read().await.notifications.clone(); - NotificationService::notify_execution_halted(notify_cfg, &ctx).await; + Self::finalize_task(&db, &config, &ctx).await; } // Fire event when CodingAgent execution has finished @@ -886,12 +919,12 @@ impl ContainerService for LocalContainerService { .await } - async fn try_commit_changes(&self, ctx: &ExecutionContext) -> Result<(), ContainerError> { + async fn try_commit_changes(&self, ctx: &ExecutionContext) -> Result { if !matches!( ctx.execution_process.run_reason, ExecutionProcessRunReason::CodingAgent | ExecutionProcessRunReason::CleanupScript, ) { - return Ok(()); + return Ok(false); } let message = match ctx.execution_process.run_reason { @@ -950,7 +983,8 @@ impl ContainerService for LocalContainerService { message ); - Ok(self.git().commit(Path::new(container_ref), &message)?) + let changes_committed = self.git().commit(Path::new(container_ref), &message)?; + Ok(changes_committed) } /// Copy files from the original project directory to the worktree diff --git a/crates/services/src/services/container.rs b/crates/services/src/services/container.rs index 4b1e359a..e6be3560 100644 --- a/crates/services/src/services/container.rs +++ b/crates/services/src/services/container.rs @@ -122,7 +122,7 @@ pub trait ContainerService { execution_process: &ExecutionProcess, ) -> Result<(), ContainerError>; - async fn try_commit_changes(&self, ctx: &ExecutionContext) -> Result<(), ContainerError>; + async fn try_commit_changes(&self, ctx: &ExecutionContext) -> Result; async fn copy_project_files( &self, diff --git a/crates/services/src/services/git.rs b/crates/services/src/services/git.rs index 02085300..74128317 100644 --- a/crates/services/src/services/git.rs +++ b/crates/services/src/services/git.rs @@ -171,7 +171,7 @@ impl GitService { Ok(()) } - pub fn commit(&self, path: &Path, message: &str) -> Result<(), GitServiceError> { + pub fn commit(&self, path: &Path, message: &str) -> Result { let repo = Repository::open(path)?; // Check if there are any changes to commit @@ -189,7 +189,7 @@ impl GitService { if !has_changes { tracing::debug!("No changes to commit!"); - return Ok(()); + return Ok(false); } // Get the current HEAD commit @@ -214,7 +214,7 @@ impl GitService { &[&parent_commit], )?; - Ok(()) + Ok(true) } /// Get diffs between branches or worktree changes diff --git a/frontend/src/components/projects/project-form-fields.tsx b/frontend/src/components/projects/project-form-fields.tsx index 9443dca1..a586bb32 100644 --- a/frontend/src/components/projects/project-form-fields.tsx +++ b/frontend/src/components/projects/project-form-fields.tsx @@ -247,9 +247,10 @@ export function ProjectFormFields({ className="w-full px-3 py-2 border border-input bg-background text-foreground rounded-md resize-vertical focus:outline-none focus:ring-2 focus:ring-ring" />

- This script will run after coding agent execution is complete. Use it - for quality assurance tasks like running linters, formatters, tests, - or other validation steps. + This script runs after coding agent execution{' '} + only if changes were made. Use it for quality + assurance tasks like running linters, formatters, tests, or other + validation steps. If no changes are made, this script is skipped.

diff --git a/frontend/src/utils/script-placeholders.ts b/frontend/src/utils/script-placeholders.ts index 4e1c5710..7c8b0462 100644 --- a/frontend/src/utils/script-placeholders.ts +++ b/frontend/src/utils/script-placeholders.ts @@ -19,7 +19,7 @@ npm run dev REM Add dev server start command here...`, cleanup: `@echo off REM Add cleanup commands here... -REM This runs after coding agent execution`, +REM This runs after coding agent execution - only if changes were made`, }; } } @@ -35,7 +35,7 @@ npm run dev # Add dev server start command here...`, cleanup: `#!/bin/bash # Add cleanup commands here... -# This runs after coding agent execution`, +# This runs after coding agent execution - only if changes were made`, }; } }