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:
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"#
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user