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