fix: add safeguards to prevent /plan from executing without a complete plan
The AI could transition to "executing" status without writing phases, commands, or context to MEMORY.md, causing empty plan execution. Adds multiple defense layers: - Block non-read/non-MEMORY.md tool calls during interview and review - Cap LLM iterations to 3 during pre-execution phases - Validate phases exist before /plan start allows transition - Auto-revert to interviewing if executing with no phases - Hijack interviewing→executing transition to "review" status, showing the plan to the user for approval via /plan start - Inject staleness nudge after 2 consecutive turns without MEMORY.md update - Add GetReviewContext() for review-phase-only system prompt injection Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7bf9eb1d85
commit
db8bc53265
5 changed files with 204 additions and 23 deletions
|
|
@ -83,8 +83,8 @@ Your workspace is at: %s
|
||||||
3. **Memory & Plans**
|
3. **Memory & Plans**
|
||||||
- Use memory/MEMORY.md for structured plans.
|
- Use memory/MEMORY.md for structured plans.
|
||||||
- If Status is "interviewing": Ask clarifying questions.
|
- If Status is "interviewing": Ask clarifying questions.
|
||||||
Update Context with answers via edit_file.
|
After each answer, use edit_file to save findings to ## Context in memory/MEMORY.md.
|
||||||
When ready, organize into Phases and set Status to "executing".
|
When you have enough information, write ## Phase and ## Commands sections into MEMORY.md, then set Status to "executing".
|
||||||
- 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:
|
- Plan format:
|
||||||
|
|
@ -339,6 +339,11 @@ func (cb *ContextBuilder) GetCurrentPhase() int {
|
||||||
return cb.memory.GetCurrentPhase()
|
return cb.memory.GetCurrentPhase()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetTotalPhases returns the total number of phases in the plan.
|
||||||
|
func (cb *ContextBuilder) GetTotalPhases() int {
|
||||||
|
return cb.memory.GetTotalPhases()
|
||||||
|
}
|
||||||
|
|
||||||
// FormatPlanDisplay returns a user-facing display of the full plan.
|
// FormatPlanDisplay returns a user-facing display of the full plan.
|
||||||
func (cb *ContextBuilder) FormatPlanDisplay() string {
|
func (cb *ContextBuilder) FormatPlanDisplay() string {
|
||||||
return cb.memory.FormatPlanDisplay()
|
return cb.memory.FormatPlanDisplay()
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ type AgentInstance struct {
|
||||||
Subagents *config.SubagentsConfig
|
Subagents *config.SubagentsConfig
|
||||||
SkillsFilter []string
|
SkillsFilter []string
|
||||||
Candidates []providers.FallbackCandidate
|
Candidates []providers.FallbackCandidate
|
||||||
|
|
||||||
|
// Interview staleness tracking: consecutive turns where MEMORY.md was not updated.
|
||||||
|
interviewStaleCount int
|
||||||
|
interviewMemoryLen int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAgentInstance creates an agent instance from config.
|
// NewAgentInstance creates an agent instance from config.
|
||||||
|
|
|
||||||
|
|
@ -492,6 +492,23 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
||||||
opts.ChatID,
|
opts.ChatID,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 2b. Interview staleness nudge: if MEMORY.md hasn't been updated for
|
||||||
|
// several consecutive turns, inject a reminder so the AI writes its findings.
|
||||||
|
const interviewStaleThreshold = 2
|
||||||
|
if agent.ContextBuilder.GetPlanStatus() == "interviewing" && agent.interviewStaleCount >= interviewStaleThreshold {
|
||||||
|
messages = append(messages, providers.Message{
|
||||||
|
Role: "user",
|
||||||
|
Content: "[System] You have been interviewing for several turns without updating memory/MEMORY.md. Please use edit_file now to save your findings to the ## Context section, or organize the plan into Phases if you have enough information.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2c. Snapshot plan status and MEMORY.md size before LLM iteration.
|
||||||
|
preStatus := agent.ContextBuilder.GetPlanStatus()
|
||||||
|
var preMemoryLen int
|
||||||
|
if preStatus == "interviewing" {
|
||||||
|
preMemoryLen = len(agent.ContextBuilder.ReadMemory())
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Save user message to session (use compact form if available)
|
// 3. Save user message to session (use compact form if available)
|
||||||
historyMsg := opts.UserMessage
|
historyMsg := opts.UserMessage
|
||||||
if opts.HistoryMessage != "" {
|
if opts.HistoryMessage != "" {
|
||||||
|
|
@ -514,8 +531,33 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
||||||
// This is controlled by the tool's Silent flag and ForUser content
|
// This is controlled by the tool's Silent flag and ForUser content
|
||||||
|
|
||||||
// 5a. Auto-advance plan phases after LLM iteration
|
// 5a. Auto-advance plan phases after LLM iteration
|
||||||
if agent.ContextBuilder.HasActivePlan() && agent.ContextBuilder.GetPlanStatus() == "executing" {
|
postStatus := agent.ContextBuilder.GetPlanStatus()
|
||||||
if agent.ContextBuilder.IsPlanComplete() {
|
if agent.ContextBuilder.HasActivePlan() && postStatus == "executing" {
|
||||||
|
// Intercept: if AI changed status from interviewing to executing,
|
||||||
|
// hijack to "review" and show the plan for user approval.
|
||||||
|
if preStatus == "interviewing" {
|
||||||
|
if agent.ContextBuilder.GetTotalPhases() == 0 {
|
||||||
|
_ = agent.ContextBuilder.SetPlanStatus("interviewing")
|
||||||
|
logger.WarnCF("agent", "Reverted plan to interviewing: no phases defined",
|
||||||
|
map[string]interface{}{"agent_id": agent.ID})
|
||||||
|
} else {
|
||||||
|
_ = agent.ContextBuilder.SetPlanStatus("review")
|
||||||
|
if !constants.IsInternalChannel(opts.Channel) {
|
||||||
|
planDisplay := agent.ContextBuilder.FormatPlanDisplay()
|
||||||
|
al.bus.PublishOutbound(bus.OutboundMessage{
|
||||||
|
Channel: opts.Channel,
|
||||||
|
ChatID: opts.ChatID,
|
||||||
|
Content: planDisplay + "\n\nUse /plan start to approve, or continue chatting to refine.",
|
||||||
|
SkipPlaceholder: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if agent.ContextBuilder.GetTotalPhases() == 0 {
|
||||||
|
// Safeguard: executing but no phases (shouldn't happen, but be safe).
|
||||||
|
_ = agent.ContextBuilder.SetPlanStatus("interviewing")
|
||||||
|
logger.WarnCF("agent", "Reverted plan to interviewing: no phases defined",
|
||||||
|
map[string]interface{}{"agent_id": agent.ID})
|
||||||
|
} else if agent.ContextBuilder.IsPlanComplete() {
|
||||||
_ = agent.ContextBuilder.ClearMemory()
|
_ = agent.ContextBuilder.ClearMemory()
|
||||||
if !constants.IsInternalChannel(opts.Channel) {
|
if !constants.IsInternalChannel(opts.Channel) {
|
||||||
al.bus.PublishOutbound(bus.OutboundMessage{
|
al.bus.PublishOutbound(bus.OutboundMessage{
|
||||||
|
|
@ -540,7 +582,21 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5b. Handle empty response
|
// 5b. Interview staleness detection: compare MEMORY.md size after iteration.
|
||||||
|
if agent.ContextBuilder.GetPlanStatus() == "interviewing" {
|
||||||
|
postMemoryLen := len(agent.ContextBuilder.ReadMemory())
|
||||||
|
if postMemoryLen == preMemoryLen {
|
||||||
|
agent.interviewStaleCount++
|
||||||
|
} else {
|
||||||
|
agent.interviewStaleCount = 0
|
||||||
|
}
|
||||||
|
agent.interviewMemoryLen = postMemoryLen
|
||||||
|
} else {
|
||||||
|
// Reset counter when not interviewing.
|
||||||
|
agent.interviewStaleCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5c. Handle empty response
|
||||||
if finalContent == "" {
|
if finalContent == "" {
|
||||||
finalContent = opts.DefaultResponse
|
finalContent = opts.DefaultResponse
|
||||||
}
|
}
|
||||||
|
|
@ -616,14 +672,24 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
var finalContent string
|
var finalContent string
|
||||||
lastReminderIdx := -1
|
lastReminderIdx := -1
|
||||||
|
|
||||||
for iteration < agent.MaxIterations {
|
// During pre-execution plan modes (interviewing/review), cap iterations
|
||||||
|
// to prevent runaway tool loops.
|
||||||
|
maxIter := agent.MaxIterations
|
||||||
|
if isPlanPreExecution(agent.ContextBuilder.GetPlanStatus()) {
|
||||||
|
const interviewMaxIter = 3
|
||||||
|
if maxIter > interviewMaxIter {
|
||||||
|
maxIter = interviewMaxIter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for iteration < maxIter {
|
||||||
iteration++
|
iteration++
|
||||||
|
|
||||||
logger.DebugCF("agent", "LLM iteration",
|
logger.DebugCF("agent", "LLM iteration",
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
"iteration": iteration,
|
"iteration": iteration,
|
||||||
"max": agent.MaxIterations,
|
"max": maxIter,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Build tool definitions
|
// Build tool definitions
|
||||||
|
|
@ -770,7 +836,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
al.bus.PublishOutbound(bus.OutboundMessage{
|
al.bus.PublishOutbound(bus.OutboundMessage{
|
||||||
Channel: opts.Channel,
|
Channel: opts.Channel,
|
||||||
ChatID: opts.ChatID,
|
ChatID: opts.ChatID,
|
||||||
Content: fmt.Sprintf("🔧 %s (%d/%d)", strings.Join(toolNames, ", "), iteration, agent.MaxIterations),
|
Content: fmt.Sprintf("🔧 %s (%d/%d)", strings.Join(toolNames, ", "), iteration, maxIter),
|
||||||
IsStatus: true,
|
IsStatus: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -825,7 +891,14 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toolResult := agent.Tools.ExecuteWithContext(ctx, tc.Name, tc.Arguments, opts.Channel, opts.ChatID, asyncCallback)
|
// Block non-allowed tools during plan interview mode.
|
||||||
|
// Only read-type tools and MEMORY.md writes are permitted.
|
||||||
|
var toolResult *tools.ToolResult
|
||||||
|
if isPlanPreExecution(agent.ContextBuilder.GetPlanStatus()) && !isToolAllowedDuringInterview(tc.Name, tc.Arguments) {
|
||||||
|
toolResult = tools.ErrorResult("Interview mode: only read tools and MEMORY.md edits are allowed. Focus on asking questions and updating the plan.")
|
||||||
|
} else {
|
||||||
|
toolResult = agent.Tools.ExecuteWithContext(ctx, tc.Name, tc.Arguments, opts.Channel, opts.ChatID, asyncCallback)
|
||||||
|
}
|
||||||
|
|
||||||
// Send ForUser content to user immediately if not Silent
|
// Send ForUser content to user immediately if not Silent
|
||||||
if !toolResult.Silent && toolResult.ForUser != "" && opts.SendResponse {
|
if !toolResult.Silent && toolResult.ForUser != "" && opts.SendResponse {
|
||||||
|
|
@ -884,7 +957,7 @@ func (al *AgentLoop) runLLMIteration(ctx context.Context, agent *AgentInstance,
|
||||||
|
|
||||||
// If max iterations exhausted with tool calls still pending,
|
// If max iterations exhausted with tool calls still pending,
|
||||||
// make one final LLM call without tools to force a text response.
|
// make one final LLM call without tools to force a text response.
|
||||||
if finalContent == "" && iteration >= agent.MaxIterations {
|
if finalContent == "" && iteration >= maxIter {
|
||||||
logger.WarnCF("agent", "Max iterations reached, forcing final response without tools",
|
logger.WarnCF("agent", "Max iterations reached, forcing final response without tools",
|
||||||
map[string]interface{}{
|
map[string]interface{}{
|
||||||
"agent_id": agent.ID,
|
"agent_id": agent.ID,
|
||||||
|
|
@ -1465,13 +1538,20 @@ func (al *AgentLoop) handlePlanCommand(args []string) (string, bool) {
|
||||||
if !agent.ContextBuilder.HasActivePlan() {
|
if !agent.ContextBuilder.HasActivePlan() {
|
||||||
return "No active plan.", true
|
return "No active plan.", true
|
||||||
}
|
}
|
||||||
if agent.ContextBuilder.GetPlanStatus() != "interviewing" {
|
status := agent.ContextBuilder.GetPlanStatus()
|
||||||
|
if status == "executing" {
|
||||||
return "Plan is already executing.", true
|
return "Plan is already executing.", true
|
||||||
}
|
}
|
||||||
|
if status != "interviewing" && status != "review" {
|
||||||
|
return fmt.Sprintf("Cannot start from status %q.", status), true
|
||||||
|
}
|
||||||
|
if agent.ContextBuilder.GetTotalPhases() == 0 {
|
||||||
|
return "Cannot start: no phases defined yet. Complete the interview first.", true
|
||||||
|
}
|
||||||
if err := agent.ContextBuilder.SetPlanStatus("executing"); err != nil {
|
if err := agent.ContextBuilder.SetPlanStatus("executing"); err != nil {
|
||||||
return fmt.Sprintf("Error: %v", err), true
|
return fmt.Sprintf("Error: %v", err), true
|
||||||
}
|
}
|
||||||
return "Plan status changed to executing.", true
|
return "Plan approved. Executing.", true
|
||||||
|
|
||||||
case "next":
|
case "next":
|
||||||
if !agent.ContextBuilder.HasActivePlan() {
|
if !agent.ContextBuilder.HasActivePlan() {
|
||||||
|
|
@ -1495,6 +1575,32 @@ func (al *AgentLoop) handlePlanCommand(args []string) (string, bool) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isPlanPreExecution returns true if the plan is in a pre-execution state
|
||||||
|
// (interviewing or review) where tool restrictions and iteration caps apply.
|
||||||
|
func isPlanPreExecution(status string) bool {
|
||||||
|
return status == "interviewing" || status == "review"
|
||||||
|
}
|
||||||
|
|
||||||
|
// isToolAllowedDuringInterview checks whether a tool call is permitted while the
|
||||||
|
// plan is in a pre-execution state. Read-type tools are always allowed. Write-type
|
||||||
|
// tools (edit_file, append_file, write_file) are only allowed when targeting MEMORY.md.
|
||||||
|
func isToolAllowedDuringInterview(toolName string, args map[string]interface{}) bool {
|
||||||
|
// Read-type tools: always allowed
|
||||||
|
switch toolName {
|
||||||
|
case "read_file", "list_dir", "web_search", "web_fetch":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write-type tools: allowed only when targeting MEMORY.md
|
||||||
|
switch toolName {
|
||||||
|
case "edit_file", "append_file", "write_file":
|
||||||
|
path, _ := args["path"].(string)
|
||||||
|
return strings.HasSuffix(path, "MEMORY.md")
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// expandPlanCommand detects "/plan <task>" (new plan start) and:
|
// expandPlanCommand detects "/plan <task>" (new plan start) and:
|
||||||
// - writes the interview seed to MEMORY.md
|
// - writes the interview seed to MEMORY.md
|
||||||
// - rewrites the message content for the LLM
|
// - rewrites the message content for the LLM
|
||||||
|
|
|
||||||
|
|
@ -995,18 +995,60 @@ func TestPlanCommand_Start(t *testing.T) {
|
||||||
al, cleanup := newTestAgentLoop(t)
|
al, cleanup := newTestAgentLoop(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
// Create interviewing plan
|
agent := al.registry.GetDefaultAgent()
|
||||||
|
|
||||||
|
// Create interviewing plan with phases (start requires phases)
|
||||||
|
plan := "# Active Plan\n\n> Task: Test task\n> Status: interviewing\n> Phase: 1\n\n## Phase 1: Setup\n- [ ] Step one\n\n## Context\n"
|
||||||
|
_ = agent.ContextBuilder.WriteMemory(plan)
|
||||||
|
|
||||||
|
// Transition to executing via /plan start
|
||||||
|
response, _ := al.handleCommand(context.Background(), bus.InboundMessage{Content: "/plan start"})
|
||||||
|
if !strings.Contains(response, "approved") {
|
||||||
|
t.Errorf("expected 'approved', got %q", response)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status := agent.ContextBuilder.GetPlanStatus(); status != "executing" {
|
||||||
|
t.Errorf("expected 'executing', got %q", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanCommand_StartFromReview(t *testing.T) {
|
||||||
|
al, cleanup := newTestAgentLoop(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
agent := al.registry.GetDefaultAgent()
|
||||||
|
|
||||||
|
// Create a plan in review status
|
||||||
|
plan := "# Active Plan\n\n> Task: Test task\n> Status: review\n> Phase: 1\n\n## Phase 1: Setup\n- [ ] Step one\n\n## Context\n"
|
||||||
|
_ = agent.ContextBuilder.WriteMemory(plan)
|
||||||
|
|
||||||
|
// Approve via /plan start
|
||||||
|
response, _ := al.handleCommand(context.Background(), bus.InboundMessage{Content: "/plan start"})
|
||||||
|
if !strings.Contains(response, "approved") {
|
||||||
|
t.Errorf("expected 'approved', got %q", response)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status := agent.ContextBuilder.GetPlanStatus(); status != "executing" {
|
||||||
|
t.Errorf("expected 'executing', got %q", status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlanCommand_StartNoPhases(t *testing.T) {
|
||||||
|
al, cleanup := newTestAgentLoop(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Create interviewing plan without phases
|
||||||
al.expandPlanCommand(bus.InboundMessage{Content: "/plan Test task"})
|
al.expandPlanCommand(bus.InboundMessage{Content: "/plan Test task"})
|
||||||
|
|
||||||
// Transition to executing
|
// Should be blocked because no phases exist
|
||||||
response, _ := al.handleCommand(context.Background(), bus.InboundMessage{Content: "/plan start"})
|
response, _ := al.handleCommand(context.Background(), bus.InboundMessage{Content: "/plan start"})
|
||||||
if !strings.Contains(response, "executing") {
|
if !strings.Contains(response, "no phases") {
|
||||||
t.Errorf("expected 'executing', got %q", response)
|
t.Errorf("expected 'no phases' error, got %q", response)
|
||||||
}
|
}
|
||||||
|
|
||||||
agent := al.registry.GetDefaultAgent()
|
agent := al.registry.GetDefaultAgent()
|
||||||
if status := agent.ContextBuilder.GetPlanStatus(); status != "executing" {
|
if status := agent.ContextBuilder.GetPlanStatus(); status != "interviewing" {
|
||||||
t.Errorf("expected 'executing', got %q", status)
|
t.Errorf("expected status to remain 'interviewing', got %q", status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1014,8 +1056,11 @@ func TestPlanCommand_StartAlreadyExecuting(t *testing.T) {
|
||||||
al, cleanup := newTestAgentLoop(t)
|
al, cleanup := newTestAgentLoop(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
// Create interviewing plan then start
|
agent := al.registry.GetDefaultAgent()
|
||||||
al.expandPlanCommand(bus.InboundMessage{Content: "/plan Test task"})
|
|
||||||
|
// Create interviewing plan with phases, then start
|
||||||
|
plan := "# Active Plan\n\n> Task: Test task\n> Status: interviewing\n> Phase: 1\n\n## Phase 1: Setup\n- [ ] Step one\n\n## Context\n"
|
||||||
|
_ = agent.ContextBuilder.WriteMemory(plan)
|
||||||
al.handleCommand(context.Background(), bus.InboundMessage{Content: "/plan start"})
|
al.handleCommand(context.Background(), bus.InboundMessage{Content: "/plan start"})
|
||||||
|
|
||||||
// Try start again
|
// Try start again
|
||||||
|
|
|
||||||
|
|
@ -375,7 +375,11 @@ func (ms *MemoryStore) GetInterviewContext() string {
|
||||||
sb.WriteString("- Environment (OS, language, runtime versions)\n")
|
sb.WriteString("- Environment (OS, language, runtime versions)\n")
|
||||||
sb.WriteString("- Tooling preferences (test framework, linter, formatter, CI)\n")
|
sb.WriteString("- Tooling preferences (test framework, linter, formatter, CI)\n")
|
||||||
sb.WriteString("- Key commands the user already runs (build, test, deploy)\n")
|
sb.WriteString("- Key commands the user already runs (build, test, deploy)\n")
|
||||||
sb.WriteString("When ready, organize into 2-5 phases with 3-5 steps each.\n")
|
sb.WriteString("\n### Rules\n")
|
||||||
|
sb.WriteString("- After each answer, use edit_file to append findings to the ## Context section of memory/MEMORY.md.\n")
|
||||||
|
sb.WriteString("- When you have enough information, use edit_file to write ## Phase, ## Commands, and ## Context sections into memory/MEMORY.md.\n")
|
||||||
|
sb.WriteString("- Organize into 2-5 phases with 3-5 steps each.\n")
|
||||||
|
sb.WriteString("- After writing Phases, set Status to executing. The system will handle the rest.\n")
|
||||||
sb.WriteString("\n### Target Format\n")
|
sb.WriteString("\n### Target Format\n")
|
||||||
sb.WriteString("```\n")
|
sb.WriteString("```\n")
|
||||||
sb.WriteString("## Phase 1: <title>\n")
|
sb.WriteString("## Phase 1: <title>\n")
|
||||||
|
|
@ -392,6 +396,20 @@ func (ms *MemoryStore) GetInterviewContext() string {
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetReviewContext returns context for injection during the review phase.
|
||||||
|
// Shows the full plan and instructs the AI to wait for user approval.
|
||||||
|
func (ms *MemoryStore) GetReviewContext() string {
|
||||||
|
content := ms.ReadLongTerm()
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("## Active Plan (awaiting approval)\n\n")
|
||||||
|
sb.WriteString(content)
|
||||||
|
sb.WriteString("\n\nThe plan is awaiting user approval.\n")
|
||||||
|
sb.WriteString("- If the user requests changes, update memory/MEMORY.md via edit_file.\n")
|
||||||
|
sb.WriteString("- Do NOT change Status yourself. The user will run /plan start to approve.\n")
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
// GetPlanContext returns context for injection during the executing phase.
|
// GetPlanContext returns context for injection during the executing phase.
|
||||||
// Only the current phase is shown in detail; completed phases are compressed
|
// Only the current phase is shown in detail; completed phases are compressed
|
||||||
// to one-line summaries; future phases are omitted.
|
// to one-line summaries; future phases are omitted.
|
||||||
|
|
@ -572,9 +590,12 @@ func (ms *MemoryStore) GetMemoryContext() string {
|
||||||
if longTerm != "" {
|
if longTerm != "" {
|
||||||
if ms.HasActivePlan() {
|
if ms.HasActivePlan() {
|
||||||
status := ms.GetPlanStatus()
|
status := ms.GetPlanStatus()
|
||||||
if status == "interviewing" {
|
switch status {
|
||||||
|
case "interviewing":
|
||||||
parts = append(parts, ms.GetInterviewContext())
|
parts = append(parts, ms.GetInterviewContext())
|
||||||
} else {
|
case "review":
|
||||||
|
parts = append(parts, ms.GetReviewContext())
|
||||||
|
default:
|
||||||
parts = append(parts, ms.GetPlanContext())
|
parts = append(parts, ms.GetPlanContext())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue