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 {
// 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<W: UiWriter>(
}
CommandResult::EnterPlanMode => {
in_plan_mode = true;
agent.set_plan_mode(true);
continue;
}
}
@@ -480,6 +482,7 @@ pub async fn run_interactive<W: UiWriter>(
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 {

View File

@@ -155,6 +155,8 @@ pub struct Agent<W: UiWriter> {
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<W: UiWriter> Agent<W> {
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<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
// =========================================================================
@@ -2901,7 +2914,8 @@ 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
// 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)
@@ -2910,6 +2924,7 @@ Skip if nothing new. Be brief."#;
return Ok(message);
}
}
}
let log_str = match &result {
Ok(s) => s.clone(),

View File

@@ -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,

View File

@@ -616,10 +616,10 @@ items:
desc: Works correctly
target: test::module
negative:
desc: Handles errors
- desc: Handles errors
target: test::module
boundary:
desc: Edge cases
- desc: Edge cases
target: test::module"#
}),
};

View File

@@ -420,10 +420,10 @@ items:
desc: Works
target: test
negative:
desc: Errors
- desc: Errors
target: test
boundary:
desc: Edge
- 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();