From 0f919237eac343b0b336aa7f66883834c53107dd Mon Sep 17 00:00:00 2001 From: "Dhanji R. Prasanna" Date: Thu, 5 Feb 2026 11:41:52 +1100 Subject: [PATCH] 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 for negative/boundary checks --- crates/g3-cli/src/interactive.rs | 3 +++ crates/g3-core/src/lib.rs | 19 +++++++++++++++++-- .../tests/mock_provider_integration_test.rs | 7 +++++-- ...stream_completion_characterization_test.rs | 8 ++++---- .../tests/tool_execution_roundtrip_test.rs | 12 ++++++------ 5 files changed, 35 insertions(+), 14 deletions(-) diff --git a/crates/g3-cli/src/interactive.rs b/crates/g3-cli/src/interactive.rs index 1407e99..c0a9857 100644 --- a/crates/g3-cli/src/interactive.rs +++ b/crates/g3-cli/src/interactive.rs @@ -415,6 +415,7 @@ pub async fn run_interactive( if approved { // Exit plan mode on successful approval in_plan_mode = false; + agent.set_plan_mode(false); // Add synthetic assistant message so LLM knows plan was approved use g3_providers::{Message, MessageRole}; @@ -448,6 +449,7 @@ pub async fn run_interactive( } CommandResult::EnterPlanMode => { in_plan_mode = true; + agent.set_plan_mode(true); continue; } } @@ -480,6 +482,7 @@ pub async fn run_interactive( if in_plan_mode { output.print("CTRL-D (exiting plan mode)"); in_plan_mode = false; + agent.set_plan_mode(false); // Continue the loop with normal prompt continue; } else { diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index ecbb7c1..5bf89a5 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -155,6 +155,8 @@ pub struct Agent { auto_memory: bool, /// Whether aggressive context dehydration is enabled (--acd flag) acd_enabled: bool, + /// Whether plan mode is active (gate blocks file changes without approved plan) + in_plan_mode: bool, /// Manager for async research tasks pending_research_manager: pending_research::PendingResearchManager, } @@ -210,6 +212,7 @@ impl Agent { agent_name: None, auto_memory: false, acd_enabled: false, + in_plan_mode: false, pending_research_manager: pending_research::PendingResearchManager::new(), } } @@ -1609,6 +1612,16 @@ impl Agent { ); } + /// 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 // ========================================================================= @@ -2901,14 +2914,16 @@ Skip if nothing new. Be brief."#; 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 { + // Check plan approval gate after tool execution (only in plan mode) + if self.in_plan_mode { + 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 { diff --git a/crates/g3-core/tests/mock_provider_integration_test.rs b/crates/g3-core/tests/mock_provider_integration_test.rs index c34f2f7..6ebedee 100644 --- a/crates/g3-core/tests/mock_provider_integration_test.rs +++ b/crates/g3-core/tests/mock_provider_integration_test.rs @@ -906,6 +906,9 @@ async fn test_plan_approval_gate_blocks_unapproved_changes() { // Set the working directory to the temp git repo 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 let mut plan = Plan::new("test-plan"); plan.items.push(PlanItem { @@ -915,8 +918,8 @@ async fn test_plan_approval_gate_blocks_unapproved_changes() { touches: vec!["src/test.rs".to_string()], checks: Checks { happy: Check::new("happy", "target"), - negative: Check::new("negative", "target"), - boundary: Check::new("boundary", "target"), + negative: vec![Check::new("negative", "target")], + boundary: vec![Check::new("boundary", "target")], }, evidence: vec![], notes: None, diff --git a/crates/g3-core/tests/stream_completion_characterization_test.rs b/crates/g3-core/tests/stream_completion_characterization_test.rs index 15192d3..e9c4cf4 100644 --- a/crates/g3-core/tests/stream_completion_characterization_test.rs +++ b/crates/g3-core/tests/stream_completion_characterization_test.rs @@ -616,11 +616,11 @@ items: desc: Works correctly target: test::module negative: - desc: Handles errors - target: test::module + - desc: Handles errors + target: test::module boundary: - desc: Edge cases - target: test::module"# + - desc: Edge cases + target: test::module"# }), }; let write_result = agent.execute_tool(&write_call).await.unwrap(); diff --git a/crates/g3-core/tests/tool_execution_roundtrip_test.rs b/crates/g3-core/tests/tool_execution_roundtrip_test.rs index 503549f..2e93a56 100644 --- a/crates/g3-core/tests/tool_execution_roundtrip_test.rs +++ b/crates/g3-core/tests/tool_execution_roundtrip_test.rs @@ -420,11 +420,11 @@ items: desc: Works target: test negative: - desc: Errors - target: test + - desc: Errors + target: test boundary: - desc: Edge - target: test"# + - desc: Edge + target: test"# }), ); @@ -477,8 +477,8 @@ items: touches: ["src/test.rs"] checks: happy: {desc: Works, target: test} - negative: {desc: Errors, target: test} - boundary: {desc: Edge, target: test}"# + negative: [{desc: Errors, target: test}] + boundary: [{desc: Edge, target: test}]"# }), ); agent.execute_tool(&write_call).await.unwrap();