fix: prevent plan mode from bypassing user approval gate
The AI could skip the "review" approval step and jump straight to "executing" because (1) the system prompt told it to set Status to "executing" after writing phases, and (2) tool-call filters inside runLLMIteration read the live plan status, which the AI could mutate mid-iteration via MEMORY.md edits. - context.go: instruct AI to transition to "review" (not "executing"), document the review state, and list it in the status format - loop.go: snapshot plan status before the iteration loop and use it for all filter/reminder decisions so mid-loop writes cannot disable the interview/review gate - loop.go: strengthen the interviewing reminder to explicitly say "change Status to review, do NOT set it to executing" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c9197b32a3
commit
f6d99f854f
2 changed files with 13 additions and 10 deletions
|
|
@ -85,13 +85,14 @@ Your workspace is at: %s
|
||||||
- NEVER remove or overwrite the header block (# Active Plan, > Task:, > Status:, > Phase:). The system parses these lines to track plan state.
|
- NEVER remove or overwrite the header block (# Active Plan, > Task:, > Status:, > Phase:). The system parses these lines to track plan state.
|
||||||
- If Status is "interviewing": Ask clarifying questions.
|
- If Status is "interviewing": Ask clarifying questions.
|
||||||
After each answer, use edit_file to save findings to ## Context in memory/MEMORY.md.
|
After each answer, use edit_file to save findings to ## Context in memory/MEMORY.md.
|
||||||
When you have enough information, add ## Phase sections with "- [ ]" checkbox steps, and ## Commands section below the header. Then change > Status: to "executing".
|
When you have enough information, add ## Phase sections with "- [ ]" checkbox steps, and ## Commands section below the header. Then change > Status: to "review".
|
||||||
|
- If Status is "review": The plan is awaiting user approval. Do NOT change Status yourself.
|
||||||
- If Status is "executing": Work through the current Phase's steps.
|
- If Status is "executing": Work through the current Phase's steps.
|
||||||
Mark each "- [x]" via edit_file. The system will auto-advance phases.
|
Mark each "- [x]" via edit_file. The system will auto-advance phases.
|
||||||
- Plan format (header is written by the system — do NOT delete it):
|
- Plan format (header is written by the system — do NOT delete it):
|
||||||
# Active Plan
|
# Active Plan
|
||||||
> Task: <description>
|
> Task: <description>
|
||||||
> Status: interviewing | executing
|
> Status: interviewing | review | executing
|
||||||
> Phase: <current phase number>
|
> Phase: <current phase number>
|
||||||
## Phase 1: <title>
|
## Phase 1: <title>
|
||||||
- [ ] Step 1
|
- [ ] Step 1
|
||||||
|
|
|
||||||
|
|
@ -823,7 +823,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Run LLM iteration loop
|
// 5. Run LLM iteration loop
|
||||||
finalContent, iteration, err := al.runLLMIteration(ctx, agent, messages, opts, task)
|
finalContent, iteration, err := al.runLLMIteration(ctx, agent, messages, opts, task, preStatus)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -993,7 +993,8 @@ func buildPlanReminder(planStatus string) (providers.Message, bool) {
|
||||||
case "interviewing":
|
case "interviewing":
|
||||||
content = "[System] You are interviewing the user to build a plan. " +
|
content = "[System] You are interviewing the user to build a plan. " +
|
||||||
"Ask clarifying questions and save findings to ## Context in memory/MEMORY.md using edit_file. " +
|
"Ask clarifying questions and save findings to ## Context in memory/MEMORY.md using edit_file. " +
|
||||||
"When you have enough information, write ## Phase sections with `- [ ]` checkbox steps, and ## Commands section."
|
"When you have enough information, write ## Phase sections with `- [ ]` checkbox steps, and ## Commands section. " +
|
||||||
|
"Then change > Status: to review. Do NOT set it to executing."
|
||||||
case "review":
|
case "review":
|
||||||
content = "[System] The plan is under review. " +
|
content = "[System] The plan is under review. " +
|
||||||
"Wait for the user to approve or request changes. Do not proceed with execution."
|
"Wait for the user to approve or request changes. Do not proceed with execution."
|
||||||
|
|
@ -1493,6 +1494,7 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
messages []providers.Message,
|
messages []providers.Message,
|
||||||
opts processOptions,
|
opts processOptions,
|
||||||
task *activeTask,
|
task *activeTask,
|
||||||
|
planSnapshot string,
|
||||||
) (string, int, error) {
|
) (string, int, error) {
|
||||||
iteration := 0
|
iteration := 0
|
||||||
var finalContent string
|
var finalContent string
|
||||||
|
|
@ -1503,7 +1505,7 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
|
|
||||||
// Snapshot unchecked step count before tool loop so we can detect progress.
|
// Snapshot unchecked step count before tool loop so we can detect progress.
|
||||||
preUnchecked := -1 // -1 = not tracking
|
preUnchecked := -1 // -1 = not tracking
|
||||||
if agent.ContextBuilder.GetPlanStatus() == "executing" {
|
if planSnapshot == "executing" {
|
||||||
preUnchecked = strings.Count(agent.ContextBuilder.ReadMemory(), "- [ ]")
|
preUnchecked = strings.Count(agent.ContextBuilder.ReadMemory(), "- [ ]")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1756,7 +1758,7 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
curUnchecked = strings.Count(agent.ContextBuilder.ReadMemory(), "- [ ]")
|
curUnchecked = strings.Count(agent.ContextBuilder.ReadMemory(), "- [ ]")
|
||||||
}
|
}
|
||||||
if curUnchecked > 0 && !planMarkNudged &&
|
if curUnchecked > 0 && !planMarkNudged &&
|
||||||
agent.ContextBuilder.GetPlanStatus() == "executing" {
|
planSnapshot == "executing" {
|
||||||
planMarkNudged = true
|
planMarkNudged = true
|
||||||
messages = append(messages, providers.Message{
|
messages = append(messages, providers.Message{
|
||||||
Role: "assistant",
|
Role: "assistant",
|
||||||
|
|
@ -1802,7 +1804,7 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
// message, the tool-result list, or the session store.
|
// message, the tool-result list, or the session store.
|
||||||
// A single compact rejection message is injected instead.
|
// A single compact rejection message is injected instead.
|
||||||
var interviewRejected []string
|
var interviewRejected []string
|
||||||
if isPlanPreExecution(agent.ContextBuilder.GetPlanStatus()) {
|
if isPlanPreExecution(planSnapshot) {
|
||||||
allowed := normalizedToolCalls[:0] // reuse backing array
|
allowed := normalizedToolCalls[:0] // reuse backing array
|
||||||
for _, tc := range normalizedToolCalls {
|
for _, tc := range normalizedToolCalls {
|
||||||
if isToolAllowedDuringInterview(tc.Name, tc.Arguments) {
|
if isToolAllowedDuringInterview(tc.Name, tc.Arguments) {
|
||||||
|
|
@ -2069,14 +2071,14 @@ func (al *AgentLoop) runLLMIteration(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject plan-mode reminder to keep AI focused on interview/review workflow.
|
// Inject plan-mode reminder to keep AI focused on interview/review workflow.
|
||||||
if iteration > 1 && isPlanPreExecution(agent.ContextBuilder.GetPlanStatus()) {
|
if iteration > 1 && isPlanPreExecution(planSnapshot) {
|
||||||
if reminder, ok := buildPlanReminder(agent.ContextBuilder.GetPlanStatus()); ok {
|
if reminder, ok := buildPlanReminder(planSnapshot); ok {
|
||||||
messages = append(messages, reminder)
|
messages = append(messages, reminder)
|
||||||
logger.DebugCF("agent", "Injected plan reminder",
|
logger.DebugCF("agent", "Injected plan reminder",
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
"plan_status": agent.ContextBuilder.GetPlanStatus(),
|
"plan_status": planSnapshot,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue