- EnterPlanModeTool: switch to read-only mode with plan file path management - ExitPlanModeTool: restore permissions and validate plan content - PermissionRuleEngine: plan file edit exception in PLAN mode - SystemPromptBuilder: 5-phase plan workflow instructions injection - /plan command: toggle plan mode from command line - Plan file stored at ~/.claude/projects/[path]/PLAN.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>pull/1/head
parent
bd98dea6b3
commit
cb76b267f1
@ -0,0 +1,48 @@ |
||||
package com.claudecode.command.impl; |
||||
|
||||
import com.claudecode.command.CommandContext; |
||||
import com.claudecode.command.SlashCommand; |
||||
import com.claudecode.permission.PermissionSettings; |
||||
import com.claudecode.permission.PermissionTypes.PermissionMode; |
||||
|
||||
/** |
||||
* /plan 命令 —— 对应 claude-code/src/commands/plan/plan.tsx。 |
||||
* <p> |
||||
* 切换计划模式开关。在计划模式下,AI只能分析不能修改。 |
||||
*/ |
||||
public class PlanCommand implements SlashCommand { |
||||
|
||||
private final PermissionSettings permissionSettings; |
||||
|
||||
public PlanCommand(PermissionSettings permissionSettings) { |
||||
this.permissionSettings = permissionSettings; |
||||
} |
||||
|
||||
@Override |
||||
public String name() { |
||||
return "plan"; |
||||
} |
||||
|
||||
@Override |
||||
public String description() { |
||||
return "Toggle plan mode (analysis only, no file modifications)"; |
||||
} |
||||
|
||||
@Override |
||||
public String execute(String args, CommandContext context) { |
||||
PermissionMode currentMode = permissionSettings.getCurrentMode(); |
||||
|
||||
if (currentMode == PermissionMode.PLAN) { |
||||
// Exit plan mode
|
||||
permissionSettings.setCurrentMode(PermissionMode.DEFAULT); |
||||
return "📋 Exited plan mode. Normal permissions restored.\n" + |
||||
"All tools are now available."; |
||||
} else { |
||||
// Enter plan mode
|
||||
permissionSettings.setCurrentMode(PermissionMode.PLAN); |
||||
return "📋 Entered plan mode.\n" + |
||||
"Only read-only tools are available. Use /plan again to exit.\n" + |
||||
"Or ask the AI to call EnterPlanMode for the full workflow."; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,150 @@ |
||||
package com.claudecode.tool.impl; |
||||
|
||||
import com.claudecode.permission.PermissionSettings; |
||||
import com.claudecode.permission.PermissionTypes.PermissionMode; |
||||
import com.claudecode.tool.ToolContext; |
||||
import com.claudecode.tool.Tool; |
||||
|
||||
import java.nio.file.Files; |
||||
import java.nio.file.Path; |
||||
import java.util.Map; |
||||
|
||||
/** |
||||
* 进入计划模式工具 —— 对应 claude-code/src/tools/EnterPlanModeTool。 |
||||
* <p> |
||||
* 将权限切换到只读模式,AI只能分析代码不能修改, |
||||
* 只有计划文件(PLAN.md)可以编辑。 |
||||
*/ |
||||
public class EnterPlanModeTool implements Tool { |
||||
|
||||
public static final String PLAN_MODE_KEY = "PLAN_MODE_ACTIVE"; |
||||
public static final String PLAN_FILE_PATH_KEY = "PLAN_FILE_PATH"; |
||||
public static final String PRE_PLAN_MODE_KEY = "PRE_PLAN_MODE"; |
||||
|
||||
@Override |
||||
public String name() { |
||||
return "EnterPlanMode"; |
||||
} |
||||
|
||||
@Override |
||||
public String description() { |
||||
return """ |
||||
Enter plan mode to analyze the codebase and design an implementation plan WITHOUT making changes. |
||||
|
||||
When to use: |
||||
- User asks you to "plan" or "think about" a change before implementing |
||||
- User wants to understand approach before committing to it |
||||
- Complex multi-file changes that need careful design |
||||
|
||||
In plan mode: |
||||
- You can ONLY use read-only tools (Read, Grep, Glob, ListFiles, WebFetch, WebSearch) |
||||
- You can ONLY write to the plan file (PLAN.md) |
||||
- All other file modifications and shell commands are BLOCKED |
||||
- Use AskUserQuestion to clarify requirements |
||||
- Call ExitPlanMode when the plan is complete |
||||
|
||||
The plan file location is determined automatically based on the project path. |
||||
"""; |
||||
} |
||||
|
||||
@Override |
||||
public String inputSchema() { |
||||
return """ |
||||
{ |
||||
"type": "object", |
||||
"properties": { |
||||
"reason": { |
||||
"type": "string", |
||||
"description": "Brief reason for entering plan mode" |
||||
} |
||||
}, |
||||
"required": [] |
||||
} |
||||
"""; |
||||
} |
||||
|
||||
@Override |
||||
public String execute(Map<String, Object> input, ToolContext context) { |
||||
// Check if already in plan mode
|
||||
Boolean active = context.getOrDefault(PLAN_MODE_KEY, false); |
||||
if (active) { |
||||
String existingPlan = context.get(PLAN_FILE_PATH_KEY); |
||||
return "Already in plan mode. Plan file: " + existingPlan; |
||||
} |
||||
|
||||
// Determine plan file path
|
||||
Path workDir = context.getWorkDir(); |
||||
Path planDir = getPlanDirectory(workDir); |
||||
Path planFile = planDir.resolve("PLAN.md"); |
||||
|
||||
// Save pre-plan mode for restoration
|
||||
PermissionSettings permSettings = context.get("PERMISSION_SETTINGS"); |
||||
if (permSettings != null) { |
||||
PermissionMode previousMode = permSettings.getCurrentMode(); |
||||
context.set(PRE_PLAN_MODE_KEY, previousMode); |
||||
// Switch to PLAN mode
|
||||
permSettings.setCurrentMode(PermissionMode.PLAN); |
||||
} |
||||
|
||||
// Store plan state
|
||||
context.set(PLAN_MODE_KEY, true); |
||||
context.set(PLAN_FILE_PATH_KEY, planFile.toString()); |
||||
|
||||
// Create plan directory if needed
|
||||
try { |
||||
Files.createDirectories(planDir); |
||||
} catch (Exception e) { |
||||
// Non-fatal
|
||||
} |
||||
|
||||
String reason = input != null ? (String) input.get("reason") : null; |
||||
boolean planExists = Files.exists(planFile); |
||||
|
||||
StringBuilder result = new StringBuilder(); |
||||
result.append("✅ Entered plan mode.\n\n"); |
||||
result.append("📋 Plan file: ").append(planFile).append("\n"); |
||||
if (planExists) { |
||||
result.append("📄 Existing plan found — you can read and update it.\n"); |
||||
} else { |
||||
result.append("📝 No existing plan — create one by writing to the plan file.\n"); |
||||
} |
||||
result.append("\n"); |
||||
result.append("Restrictions active:\n"); |
||||
result.append(" • Only read-only tools allowed (Read, Grep, Glob, etc.)\n"); |
||||
result.append(" • Only the plan file can be edited\n"); |
||||
result.append(" • Shell commands are blocked\n"); |
||||
result.append(" • Call ExitPlanMode when your plan is ready\n"); |
||||
|
||||
if (reason != null && !reason.isBlank()) { |
||||
result.append("\nReason: ").append(reason); |
||||
} |
||||
|
||||
return result.toString(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isReadOnly() { |
||||
// This tool itself doesn't modify files
|
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public String activityDescription(Map<String, Object> input) { |
||||
return "Entering plan mode..."; |
||||
} |
||||
|
||||
/** |
||||
* Get plan directory for the given work directory. |
||||
* Uses ~/.claude/projects/[sanitized-path]/ structure. |
||||
*/ |
||||
static Path getPlanDirectory(Path workDir) { |
||||
String sanitized = workDir.toAbsolutePath().toString() |
||||
.replace(":", "_") |
||||
.replace("\\", "_") |
||||
.replace("/", "_"); |
||||
return Path.of(System.getProperty("user.home")) |
||||
.resolve(".claude") |
||||
.resolve("projects") |
||||
.resolve(sanitized); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue