Skip to content

Commit da9a47c

Browse files
authored
feat(plan-mode): Persist plans as markdown and fix approval UX (#461)
## Summary - Closes #434 — `RequestPlanApproval` now writes the plan to `<configDir>/plans/<timestamp>-<slug>.md` (atomic write) and reads it back as canonical content; the default plan-mode prompt mandates an 8-section H2 template (Context, Files to Modify, Current Code, Changes, Performance Impact, Critical Files, Edge Cases, Verification). - Fixes several pre-existing plan-mode bugs surfaced during end-to-end testing: dropped `reasoning_content` on the post-approval turn (DeepSeek 400); race that marked the wrong assistant message as `IsPlan`; approval-button navigation didn't refresh the viewport; up/down arrow triggered history navigation while approval was pending; the plan body was rendered with an empty visible region above the buttons. - Adds `docs/plan-mode.md`, a `RequestPlanApproval` entry in `docs/tools-reference.md`, and a `CLAUDE.md` pointer; adds `plans/` to `.infer/.gitignore` (project + `cmd/init.go` embedded copy).
1 parent 05fe627 commit da9a47c

16 files changed

Lines changed: 1120 additions & 126 deletions

.infer/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ conversations.db*
66
conversations
77
bin/
88
tmp/
9+
plans/

.infer/prompts.yaml

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ agent:
99
CAPABILITIES IN PLAN MODE:
1010
- Read, Grep, and Tree tools for gathering information
1111
- TodoWrite for tracking planning progress
12-
- RequestPlanApproval tool to submit your plan for user approval
12+
- RequestPlanApproval tool to submit your plan for user approval (also persists the plan as a Markdown file under <configDir>/plans/)
1313
- Analyze code structure and dependencies
1414
- Break down complex tasks into concrete, executable steps
1515
- Identify exact files and code locations that need changes
@@ -23,28 +23,43 @@ agent:
2323
PLANNING WORKFLOW:
2424
1. Use Read/Grep/Tree to understand the codebase thoroughly
2525
2. Analyze the user's request and identify ALL requirements
26-
3. If you need clarification or more information, ASK the user - do NOT call RequestPlanApproval yet
27-
4. Break down into specific, numbered action steps
28-
5. For EACH step, specify:
29-
- Exact file paths to modify
30-
- Specific changes to make
31-
- Tool calls that will be needed
32-
6. Include testing and validation steps
33-
7. When your plan is complete and actionable, call RequestPlanApproval tool
26+
3. If you need clarification or more information, ASK the user in a regular assistant turn - do NOT call RequestPlanApproval yet
27+
4. Iterate with the user until the plan is complete and unambiguous
28+
5. When the plan is final, call RequestPlanApproval with both a short title AND the Markdown plan body
3429
3530
DECISION MAKING:
3631
- Need more info? ASK questions instead of requesting approval
3732
- Plan has gaps or uncertainties? ASK for clarification
3833
- Plan is complete and specific? Call RequestPlanApproval tool
3934
40-
OUTPUT FORMAT - ACTIONABLE STEPS:
41-
Structure your plan with concrete actions:
42-
- Overview: What will be done and why
43-
- Steps: Numbered steps with SPECIFIC actions
44-
Example: "Step 1: Edit /path/to/file.go - Add function X at line Y"
45-
Example: "Step 2: Run 'task test' to verify changes"
46-
- Files: Exact list of files to be modified
47-
- Testing: Specific commands to run and expected outcomes
35+
OUTPUT FORMAT - MARKDOWN PLAN:
36+
The 'plan' argument MUST be a Markdown document using the following H2 sections, in this order. Omit any section that does not apply to the task; never invent extra top-level sections.
37+
38+
## Context
39+
Why this change is being made — the problem, the trigger, the intended outcome.
40+
41+
## Files to Modify
42+
Bullet list of exact file paths that will change, each with a one-line note on the kind of change.
43+
44+
## Current Code
45+
Short, relevant snippets of the existing code being changed (with file:line references). Skip when not applicable (e.g. brand-new files).
46+
47+
## Changes
48+
The concrete edits, grouped per file or per concern. Be specific: function names, signatures, what is added/removed/replaced.
49+
50+
## Performance Impact
51+
Expected runtime, memory, I/O, or token-usage impact. Write "Negligible." if there isn't any.
52+
53+
## Critical Files
54+
Files that other code depends on and that must remain backward-compatible (e.g. shared interfaces, public APIs). Skip when not applicable.
55+
56+
## Edge Cases
57+
Inputs and conditions that need explicit handling, plus how the plan handles them.
58+
59+
## Verification
60+
Concrete steps the user can run to confirm the change works end-to-end (commands, tests, manual checks).
61+
62+
The 'title' argument MUST be a short human-readable phrase (≤ 60 chars, no slashes). It becomes the H1 of the saved file and the basis of the on-disk filename.
4863
4964
REMEMBER:
5065
- If accepted, YOU will execute this plan. Make it specific and actionable!
@@ -305,15 +320,19 @@ tools:
305320
When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.
306321
RequestPlanApproval:
307322
description: |-
308-
Submit your completed plan for user approval.
323+
Submit your completed plan for user approval and persist it to disk.
309324
310325
What happens:
311-
- Your plan will be displayed to the user
312-
- User can approve or reject
326+
- The plan is written as a Markdown file to <configDir>/plans/<timestamp>-<slug>.md
327+
- The plan is displayed to the user with Accept / Reject / Accept-and-Auto-Approve options
313328
- If approved, you'll switch to execution mode with full tool access
314-
- If rejected, user will provide feedback
329+
- If rejected, the file remains on disk as an audit trail and the user provides feedback
330+
331+
Required parameters:
332+
- title: A short human-readable phrase (≤ 60 chars, no slashes). Becomes the H1 heading and the filename slug.
333+
- plan: The full plan as Markdown using H2 sections in this order — ## Context, ## Files to Modify, ## Current Code, ## Changes, ## Performance Impact, ## Critical Files, ## Edge Cases, ## Verification. Omit any section that is not applicable.
315334
316-
Include your complete plan in the 'plan' parameter.
335+
Only call this tool when the plan is final. If you need clarification, ask the user in a normal assistant turn first.
317336
WebFetch:
318337
description: Fetch content from whitelisted URLs. Set download=true to save the file to disk automatically. Useful for downloading A2A task artifacts or other files.
319338
WebSearch:

CLAUDE.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,33 @@ images that don't ship `/usr/share/zoneinfo`.
595595
...)` calls in `cmd/root.go` — without those, viper unmarshals an empty
596596
config and the defaults function's values are ignored.
597597

598+
## Plan Mode
599+
600+
Plan mode (`AgentModePlan` in `internal/domain/state.go`) is a read-only
601+
operating mode the user enters via Shift+Tab in the chat TUI. The model
602+
gets `Read`/`Grep`/`Tree`/`TodoWrite` plus the `RequestPlanApproval` tool
603+
and is otherwise blocked from any mutating tools (enforced in
604+
`internal/services/tools.go::FilterToolsForMode`).
605+
606+
When the model calls `RequestPlanApproval`, the tool persists the plan as
607+
a Markdown file under `<configDir>/plans/<YYYY-MM-DD-HHMMSS>-<slug>.md`
608+
(atomic write: `.tmp` → `os.Rename`). The plan body must follow a fixed
609+
8-section H2 template (Context, Files to Modify, Current Code, Changes,
610+
Performance Impact, Critical Files, Edge Cases, Verification) — see
611+
`config/prompts.go::DefaultPromptsConfig` for the prompt that pins this
612+
contract.
613+
614+
- Tool: `internal/agent/tools/request_plan_approval.go`
615+
- System prompt: `config/prompts.go` (`agent.system_prompt_plan`)
616+
- Approval event flow: `internal/agent/agent.go`
617+
`PlanApprovalRequestedEvent` → `internal/handlers/chat_handler.go`
618+
`HandlePlanApprovalRequestedEvent` / `HandlePlanApprovalResponseEvent`
619+
- UI state: `domain.PlanApprovalUIState`, `ViewStatePlanApproval`
620+
621+
Rejected plans stay on disk as an audit trail — by design.
622+
623+
See `docs/plan-mode.md` for the full user-facing guide.
624+
598625
## Model Thinking Visualization
599626

600627
When models use extended thinking (reasoning), their internal thought process is displayed as collapsible blocks above responses.

cmd/init.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ conversations.db*
9595
conversations
9696
bin/
9797
tmp/
98+
plans/
9899
`
99100

100101
if err := os.WriteFile(gitignorePath, []byte(gitignoreContent), 0644); err != nil {

config/prompts.go

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ type PromptsToolsConfig struct {
191191
// DefaultPromptsConfig returns the in-code default prompts. This is the
192192
// single source of truth — `infer init` seeds prompts.yaml from this and
193193
// the runtime overlay falls back to it when fields are missing.
194-
func DefaultPromptsConfig() *PromptsConfig {
194+
func DefaultPromptsConfig() *PromptsConfig { //nolint:funlen
195195
return &PromptsConfig{
196196
Agent: PromptsAgentConfig{
197197
SystemPrompt: `Autonomous software engineering agent. Execute tasks iteratively until completion.`,
@@ -202,7 +202,7 @@ CRITICAL: Your plan MUST be actionable - if the user accepts it, you will be ask
202202
CAPABILITIES IN PLAN MODE:
203203
- Read, Grep, and Tree tools for gathering information
204204
- TodoWrite for tracking planning progress
205-
- RequestPlanApproval tool to submit your plan for user approval
205+
- RequestPlanApproval tool to submit your plan for user approval (also persists the plan as a Markdown file under <configDir>/plans/)
206206
- Analyze code structure and dependencies
207207
- Break down complex tasks into concrete, executable steps
208208
- Identify exact files and code locations that need changes
@@ -216,28 +216,43 @@ RESTRICTIONS IN PLAN MODE:
216216
PLANNING WORKFLOW:
217217
1. Use Read/Grep/Tree to understand the codebase thoroughly
218218
2. Analyze the user's request and identify ALL requirements
219-
3. If you need clarification or more information, ASK the user - do NOT call RequestPlanApproval yet
220-
4. Break down into specific, numbered action steps
221-
5. For EACH step, specify:
222-
- Exact file paths to modify
223-
- Specific changes to make
224-
- Tool calls that will be needed
225-
6. Include testing and validation steps
226-
7. When your plan is complete and actionable, call RequestPlanApproval tool
219+
3. If you need clarification or more information, ASK the user in a regular assistant turn - do NOT call RequestPlanApproval yet
220+
4. Iterate with the user until the plan is complete and unambiguous
221+
5. When the plan is final, call RequestPlanApproval with both a short title AND the Markdown plan body
227222
228223
DECISION MAKING:
229224
- Need more info? ASK questions instead of requesting approval
230225
- Plan has gaps or uncertainties? ASK for clarification
231226
- Plan is complete and specific? Call RequestPlanApproval tool
232227
233-
OUTPUT FORMAT - ACTIONABLE STEPS:
234-
Structure your plan with concrete actions:
235-
- Overview: What will be done and why
236-
- Steps: Numbered steps with SPECIFIC actions
237-
Example: "Step 1: Edit /path/to/file.go - Add function X at line Y"
238-
Example: "Step 2: Run 'task test' to verify changes"
239-
- Files: Exact list of files to be modified
240-
- Testing: Specific commands to run and expected outcomes
228+
OUTPUT FORMAT - MARKDOWN PLAN:
229+
The 'plan' argument MUST be a Markdown document using the following H2 sections, in this order. Omit any section that does not apply to the task; never invent extra top-level sections.
230+
231+
## Context
232+
Why this change is being made — the problem, the trigger, the intended outcome.
233+
234+
## Files to Modify
235+
Bullet list of exact file paths that will change, each with a one-line note on the kind of change.
236+
237+
## Current Code
238+
Short, relevant snippets of the existing code being changed (with file:line references). Skip when not applicable (e.g. brand-new files).
239+
240+
## Changes
241+
The concrete edits, grouped per file or per concern. Be specific: function names, signatures, what is added/removed/replaced.
242+
243+
## Performance Impact
244+
Expected runtime, memory, I/O, or token-usage impact. Write "Negligible." if there isn't any.
245+
246+
## Critical Files
247+
Files that other code depends on and that must remain backward-compatible (e.g. shared interfaces, public APIs). Skip when not applicable.
248+
249+
## Edge Cases
250+
Inputs and conditions that need explicit handling, plus how the plan handles them.
251+
252+
## Verification
253+
Concrete steps the user can run to confirm the change works end-to-end (commands, tests, manual checks).
254+
255+
The 'title' argument MUST be a short human-readable phrase (≤ 60 chars, no slashes). It becomes the H1 of the saved file and the basis of the on-disk filename.
241256
242257
REMEMBER:
243258
- If accepted, YOU will execute this plan. Make it specific and actionable!
@@ -505,15 +520,19 @@ NOTE that you should not use this tool if there is only one trivial task to do.
505520
When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.`,
506521
},
507522
RequestPlanApproval: PromptsToolDescription{
508-
Description: `Submit your completed plan for user approval.
523+
Description: `Submit your completed plan for user approval and persist it to disk.
509524
510525
What happens:
511-
- Your plan will be displayed to the user
512-
- User can approve or reject
526+
- The plan is written as a Markdown file to <configDir>/plans/<timestamp>-<slug>.md
527+
- The plan is displayed to the user with Accept / Reject / Accept-and-Auto-Approve options
513528
- If approved, you'll switch to execution mode with full tool access
514-
- If rejected, user will provide feedback
529+
- If rejected, the file remains on disk as an audit trail and the user provides feedback
530+
531+
Required parameters:
532+
- title: A short human-readable phrase (≤ 60 chars, no slashes). Becomes the H1 heading and the filename slug.
533+
- plan: The full plan as Markdown using H2 sections in this order — ## Context, ## Files to Modify, ## Current Code, ## Changes, ## Performance Impact, ## Critical Files, ## Edge Cases, ## Verification. Omit any section that is not applicable.
515534
516-
Include your complete plan in the 'plan' parameter.`,
535+
Only call this tool when the plan is final. If you need clarification, ask the user in a normal assistant turn first.`,
517536
},
518537
WebFetch: PromptsToolDescription{
519538
Description: `Fetch content from whitelisted URLs. Set download=true to save the file to disk automatically. Useful for downloading A2A task artifacts or other files.`,

config/prompts_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,42 @@ func TestDefaultPromptsConfig_AllPromptsPopulated(t *testing.T) {
5959
}
6060
}
6161

62+
// The plan-mode prompt advertises a fixed Markdown section template to
63+
// the model. This guards the template against accidental edits and makes
64+
// the contract with docs/plan-mode.md explicit.
65+
func TestDefaultPromptsConfig_PlanPromptStructure(t *testing.T) {
66+
cfg := config.DefaultPromptsConfig()
67+
plan := cfg.Agent.SystemPromptPlan
68+
69+
wantSections := []string{
70+
"## Context",
71+
"## Files to Modify",
72+
"## Current Code",
73+
"## Changes",
74+
"## Performance Impact",
75+
"## Critical Files",
76+
"## Edge Cases",
77+
"## Verification",
78+
}
79+
for _, section := range wantSections {
80+
if !strings.Contains(plan, section) {
81+
t.Errorf("plan-mode system prompt missing section heading %q", section)
82+
}
83+
}
84+
85+
if !strings.Contains(plan, "title") {
86+
t.Errorf("plan-mode system prompt should mention the 'title' parameter")
87+
}
88+
89+
desc := cfg.Tools.RequestPlanApproval.Description
90+
if !strings.Contains(desc, "title") || !strings.Contains(desc, "plan") {
91+
t.Errorf("RequestPlanApproval description should mention both 'title' and 'plan' parameters, got %q", desc)
92+
}
93+
if !strings.Contains(desc, "<configDir>/plans/") {
94+
t.Errorf("RequestPlanApproval description should mention the on-disk path, got %q", desc)
95+
}
96+
}
97+
6298
// custom_instructions is intentionally empty - it's a user-supplied
6399
// opt-in. This guards it in the opposite direction so a future "fill in
64100
// a default" change is intentional.

0 commit comments

Comments
 (0)