Make plan approval gate only active in plan mode

- Add in_plan_mode flag to Agent struct
- Add set_plan_mode() and is_plan_mode() methods
- Gate check now only runs when in_plan_mode is true
- CLI calls set_plan_mode(true) on /plan command and EnterPlanMode
- CLI calls set_plan_mode(false) on approval and CTRL-D exit
- Update integration test to enable plan mode
- Fix test YAML to use Vec<Check> for negative/boundary checks
This commit is contained in:
Dhanji R. Prasanna
2026-02-05 11:41:52 +11:00
parent 3d284b8b60
commit 0f919237ea
5 changed files with 35 additions and 14 deletions

View File

@@ -415,6 +415,7 @@ pub async fn run_interactive<W: UiWriter>(
if approved { if approved {
// Exit plan mode on successful approval // Exit plan mode on successful approval
in_plan_mode = false; in_plan_mode = false;
agent.set_plan_mode(false);
// Add synthetic assistant message so LLM knows plan was approved // Add synthetic assistant message so LLM knows plan was approved
use g3_providers::{Message, MessageRole}; use g3_providers::{Message, MessageRole};
@@ -448,6 +449,7 @@ pub async fn run_interactive<W: UiWriter>(
} }
CommandResult::EnterPlanMode => { CommandResult::EnterPlanMode => {
in_plan_mode = true; in_plan_mode = true;
agent.set_plan_mode(true);
continue; continue;
} }
} }
@@ -480,6 +482,7 @@ pub async fn run_interactive<W: UiWriter>(
if in_plan_mode { if in_plan_mode {
output.print("CTRL-D (exiting plan mode)"); output.print("CTRL-D (exiting plan mode)");
in_plan_mode = false; in_plan_mode = false;
agent.set_plan_mode(false);
// Continue the loop with normal prompt // Continue the loop with normal prompt
continue; continue;
} else { } else {

View File

@@ -155,6 +155,8 @@ pub struct Agent<W: UiWriter> {
auto_memory: bool, auto_memory: bool,
/// Whether aggressive context dehydration is enabled (--acd flag) /// Whether aggressive context dehydration is enabled (--acd flag)
acd_enabled: bool, acd_enabled: bool,
/// Whether plan mode is active (gate blocks file changes without approved plan)
in_plan_mode: bool,
/// Manager for async research tasks /// Manager for async research tasks
pending_research_manager: pending_research::PendingResearchManager, pending_research_manager: pending_research::PendingResearchManager,
} }
@@ -210,6 +212,7 @@ impl<W: UiWriter> Agent<W> {
agent_name: None, agent_name: None,
auto_memory: false, auto_memory: false,
acd_enabled: false, acd_enabled: false,
in_plan_mode: false,
pending_research_manager: pending_research::PendingResearchManager::new(), pending_research_manager: pending_research::PendingResearchManager::new(),
} }
} }
@@ -1609,6 +1612,16 @@ impl<W: UiWriter> Agent<W> {
); );
} }
/// Enable or disable plan mode (blocks file changes without approved plan)
pub fn set_plan_mode(&mut self, enabled: bool) {
self.in_plan_mode = enabled;
}
/// Check if plan mode is active
pub fn is_plan_mode(&self) -> bool {
self.in_plan_mode
}
// ========================================================================= // =========================================================================
// STREAMING & LLM INTERACTION // STREAMING & LLM INTERACTION
// ========================================================================= // =========================================================================
@@ -2901,14 +2914,16 @@ Skip if nothing new. Be brief."#;
let result = self.execute_tool_inner_in_dir(tool_call, working_dir).await; let result = self.execute_tool_inner_in_dir(tool_call, working_dir).await;
// Check plan approval gate after tool execution // Check plan approval gate after tool execution (only in plan mode)
if let Some(session_id) = &self.session_id { if self.in_plan_mode {
if let Some(session_id) = &self.session_id {
if let ApprovalGateResult::Blocked { message, .. } = if let ApprovalGateResult::Blocked { message, .. } =
check_plan_approval_gate(session_id, working_dir) check_plan_approval_gate(session_id, working_dir)
{ {
// Return the blocking message instead of the tool result // Return the blocking message instead of the tool result
return Ok(message); return Ok(message);
} }
}
} }
let log_str = match &result { let log_str = match &result {

View File

@@ -906,6 +906,9 @@ async fn test_plan_approval_gate_blocks_unapproved_changes() {
// Set the working directory to the temp git repo // Set the working directory to the temp git repo
agent.set_working_dir(temp_path.to_string_lossy().to_string()); agent.set_working_dir(temp_path.to_string_lossy().to_string());
// Enable plan mode (required for the gate check to run)
agent.set_plan_mode(true);
// Create an unapproved plan for this session // Create an unapproved plan for this session
let mut plan = Plan::new("test-plan"); let mut plan = Plan::new("test-plan");
plan.items.push(PlanItem { plan.items.push(PlanItem {
@@ -915,8 +918,8 @@ async fn test_plan_approval_gate_blocks_unapproved_changes() {
touches: vec!["src/test.rs".to_string()], touches: vec!["src/test.rs".to_string()],
checks: Checks { checks: Checks {
happy: Check::new("happy", "target"), happy: Check::new("happy", "target"),
negative: Check::new("negative", "target"), negative: vec![Check::new("negative", "target")],
boundary: Check::new("boundary", "target"), boundary: vec![Check::new("boundary", "target")],
}, },
evidence: vec![], evidence: vec![],
notes: None, notes: None,

View File

@@ -616,11 +616,11 @@ items:
desc: Works correctly desc: Works correctly
target: test::module target: test::module
negative: negative:
desc: Handles errors - desc: Handles errors
target: test::module target: test::module
boundary: boundary:
desc: Edge cases - desc: Edge cases
target: test::module"# target: test::module"#
}), }),
}; };
let write_result = agent.execute_tool(&write_call).await.unwrap(); let write_result = agent.execute_tool(&write_call).await.unwrap();

View File

@@ -420,11 +420,11 @@ items:
desc: Works desc: Works
target: test target: test
negative: negative:
desc: Errors - desc: Errors
target: test target: test
boundary: boundary:
desc: Edge - desc: Edge
target: test"# target: test"#
}), }),
); );
@@ -477,8 +477,8 @@ items:
touches: ["src/test.rs"] touches: ["src/test.rs"]
checks: checks:
happy: {desc: Works, target: test} happy: {desc: Works, target: test}
negative: {desc: Errors, target: test} negative: [{desc: Errors, target: test}]
boundary: {desc: Edge, target: test}"# boundary: [{desc: Edge, target: test}]"#
}), }),
); );
agent.execute_tool(&write_call).await.unwrap(); agent.execute_tool(&write_call).await.unwrap();