diff --git a/AGENTS.md b/AGENTS.md index af1e071..d06f76c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -33,6 +33,8 @@ REPL 内所有 slash command 由程序直接处理,不进入 agent loop。 写操作和 Bash 默认需要确认,可通过 `--auto-approve` 或 `/permissions auto-on` 跳过。 +支持 `/mode build` 和 `/mode plan` 两种模式,`plan` 为只读规划模式。 + 未明确说明时,使用以下默认值: - 工作目录:当前进程目录 diff --git a/SKILLS/cli-core.md b/SKILLS/cli-core.md index d2f4179..6e28bcb 100644 --- a/SKILLS/cli-core.md +++ b/SKILLS/cli-core.md @@ -26,6 +26,7 @@ - 会话与 memory 管理优先使用 slash command,而不是自然语言或 `Bash` 探查对应文件 - 长期有价值的项目约束或偏好,优先使用 `/remember` 保存 - 高风险操作会触发确认,优先先读再改,减少无意义审批 +- 复杂任务可先切换到 `/mode plan` 进行规划,再切换回 `/mode build` 执行 - Windows 下先验证 shell 兼容性,再选择命令写法 ## 沟通风格 diff --git a/src/cc_slim/commands.py b/src/cc_slim/commands.py index e519fe6..b10b31f 100644 --- a/src/cc_slim/commands.py +++ b/src/cc_slim/commands.py @@ -7,6 +7,7 @@ from rich.console import Console from cc_slim.engine import Agent from cc_slim.memory import MemoryStore +from cc_slim.mode import ModeState from cc_slim.permissions import PermissionChecker from cc_slim.session import SessionStore @@ -27,6 +28,7 @@ def handle_command( config: Any, store: SessionStore, memory: MemoryStore, + mode: ModeState, permissions: PermissionChecker, agent: Agent, build_agent: Any, @@ -47,6 +49,11 @@ def handle_command( console.print("/new - 创建全新 session") console.print("/remember - 保存长期 memory") console.print("/memory - 查看当前项目 memory") + console.print("/mode - 查看当前模式") + console.print("/mode build - 切换到 build 模式") + console.print("/mode plan - 切换到 plan 模式") + console.print("/build - 切换到 build 模式") + console.print("/plan - 切换到 plan 模式") console.print("/permissions - 查看当前权限状态") console.print("/permissions auto-on - 开启自动批准") console.print("/permissions auto-off - 关闭自动批准") @@ -55,7 +62,26 @@ def handle_command( if name in {"/clear", "/new"}: session_meta = store.create_session(config.model) console.print(f"已创建新 session: {session_meta.get('session_id')}") - return build_agent(root, config, store, session_meta, []) + return build_agent(root, config, store, permissions, session_meta, []) + + if name in {"/build", "/plan"}: + target_mode = "build" if name == "/build" else "plan" + mode.set_mode(target_mode) + permissions.set_mode(target_mode) + console.print(f"当前模式: {target_mode}") + return agent + + if name == "/mode": + if not args: + console.print(f"当前模式: {mode.mode}") + return agent + if args in {"build", "plan"}: + mode.set_mode(args) + permissions.set_mode(args) + console.print(f"当前模式: {args}") + return agent + console.print("[red]error:[/red] 仅支持 /mode build 或 /mode plan") + return agent if name == "/history": render_history(store) @@ -69,7 +95,7 @@ def handle_command( session_meta = store.load_meta(session_id) restored_history = store.load_messages(session_id) console.print(f"已恢复 session: {session_meta.get('session_id')}") - return build_agent(root, config, store, session_meta, restored_history) + return build_agent(root, config, store, permissions, session_meta, restored_history) if name == "/memory": content = memory.read() @@ -88,6 +114,7 @@ def handle_command( status = permissions.status() console.print(f"auto_approve: {status['auto_approve']}") + console.print(f"mode: {status['mode']}") console.print(f"自动允许: {', '.join(status['auto_allowed_tools'])}") console.print(f"需要确认: {', '.join(status['confirm_required_tools'])}") return agent diff --git a/src/cc_slim/engine.py b/src/cc_slim/engine.py index 86de4c8..b5c7682 100644 --- a/src/cc_slim/engine.py +++ b/src/cc_slim/engine.py @@ -271,11 +271,13 @@ class Agent: return result def _run_tool(self, name: str, payload: dict[str, Any]) -> str: + if self.permission_checker and not self.permission_checker.is_allowed(name): + return self.permission_checker.denial_reason(name) if self.permission_checker and self.permission_checker.requires_confirmation(name): if not self.confirm_tool: - return f"Permission denied for tool: {name}" + return self.permission_checker.denial_reason(name) if not self.confirm_tool(name, payload): - return f"Permission denied for tool: {name}" + return self.permission_checker.denial_reason(name) tool = self.tools.get(name) if not tool: return f"Tool not found: {name}" diff --git a/src/cc_slim/main.py b/src/cc_slim/main.py index cc76744..b54f3c6 100644 --- a/src/cc_slim/main.py +++ b/src/cc_slim/main.py @@ -10,6 +10,7 @@ from rich.table import Table from cc_slim.commands import handle_command, parse_command from cc_slim.engine import Agent, resolve_config from cc_slim.memory import MemoryStore +from cc_slim.mode import ModeState from cc_slim.permissions import PermissionChecker from cc_slim.session import SessionStore from cc_slim.tools import build_default_tools @@ -106,7 +107,9 @@ def run( root = cwd.resolve() store = SessionStore(root) memory = MemoryStore(root) + mode = ModeState() permissions = PermissionChecker(auto_approve=auto_approve) + permissions.set_mode(mode.mode) if history: render_history(store) return @@ -159,6 +162,7 @@ def run( config=config, store=store, memory=memory, + mode=mode, permissions=permissions, agent=agent, build_agent=build_agent, diff --git a/src/cc_slim/mode.py b/src/cc_slim/mode.py new file mode 100644 index 0000000..4766f57 --- /dev/null +++ b/src/cc_slim/mode.py @@ -0,0 +1,13 @@ +from __future__ import annotations + + +class ModeState: + def __init__(self, mode: str = "build") -> None: + self.mode = mode if mode in {"build", "plan"} else "build" + + def set_mode(self, mode: str) -> None: + if mode in {"build", "plan"}: + self.mode = mode + + def is_plan(self) -> bool: + return self.mode == "plan" diff --git a/src/cc_slim/permissions.py b/src/cc_slim/permissions.py index 92774e0..dc00f85 100644 --- a/src/cc_slim/permissions.py +++ b/src/cc_slim/permissions.py @@ -4,9 +4,20 @@ from __future__ import annotations class PermissionChecker: def __init__(self, auto_approve: bool = False) -> None: self.auto_approve = auto_approve + self.mode = "build" self.auto_allowed_tools = ["Read", "Glob", "Grep"] self.confirm_required_tools = ["Write", "Edit", "Bash"] + def is_allowed(self, tool_name: str) -> bool: + if self.mode == "plan" and tool_name in self.confirm_required_tools: + return False + return True + + def denial_reason(self, tool_name: str) -> str: + if self.mode == "plan" and tool_name in self.confirm_required_tools: + return f"Tool blocked in plan mode: {tool_name}" + return f"Permission denied for tool: {tool_name}" + def requires_confirmation(self, tool_name: str) -> bool: if self.auto_approve: return False @@ -15,9 +26,14 @@ class PermissionChecker: def set_auto_approve(self, enabled: bool) -> None: self.auto_approve = enabled + def set_mode(self, mode: str) -> None: + if mode in {"build", "plan"}: + self.mode = mode + def status(self) -> dict[str, object]: return { "auto_approve": self.auto_approve, + "mode": self.mode, "auto_allowed_tools": list(self.auto_allowed_tools), "confirm_required_tools": list(self.confirm_required_tools), }