Add plan approval gate to block file changes without approved plan

- Add check_plan_approval_gate() in tools/plan.rs that runs after each tool call
- Detects file changes via git status --porcelain when plan exists but not approved
- Reverts changes: git checkout for modified files, rm for new untracked files
- Returns blocking message instructing LLM to create/approve plan first
- Add ApprovalGateResult enum with Allowed/Blocked/NotGitRepo variants
- Add set_session_id() and set_working_dir() methods on Agent for testing
- Add integration test using MockProvider to simulate blocked write_file
This commit is contained in:
Dhanji R. Prasanna
2026-02-05 11:34:10 +11:00
parent add8060526
commit c347a73cbd
5 changed files with 423 additions and 3 deletions

View File

@@ -47,6 +47,7 @@ pub use prompts::get_agent_system_prompt;
#[cfg(test)]
mod task_result_comprehensive_tests;
use crate::ui_writer::UiWriter;
use tools::plan::{check_plan_approval_gate, ApprovalGateResult};
#[cfg(test)]
mod tilde_expansion_tests;
@@ -753,6 +754,16 @@ impl<W: UiWriter> Agent<W> {
self.session_id.as_deref()
}
/// Set the session ID (useful for testing)
pub fn set_session_id(&mut self, session_id: String) {
self.session_id = Some(session_id);
}
/// Set the working directory (useful for testing)
pub fn set_working_dir(&mut self, working_dir: String) {
self.working_dir = Some(working_dir);
}
// =========================================================================
// TASK EXECUTION
// =========================================================================
@@ -2889,6 +2900,17 @@ Skip if nothing new. Be brief."#;
self.tool_calls_this_turn.push(tool_call.tool.clone());
let result = self.execute_tool_inner_in_dir(tool_call, working_dir).await;
// Check plan approval gate after tool execution
if let Some(session_id) = &self.session_id {
if let ApprovalGateResult::Blocked { message, .. } =
check_plan_approval_gate(session_id, working_dir)
{
// Return the blocking message instead of the tool result
return Ok(message);
}
}
let log_str = match &result {
Ok(s) => s.clone(),
Err(e) => format!("ERROR: {}", e),