ripperdoc 0.2.3__py3-none-any.whl → 0.2.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. ripperdoc/__init__.py +1 -1
  2. ripperdoc/__main__.py +0 -5
  3. ripperdoc/cli/cli.py +37 -16
  4. ripperdoc/cli/commands/__init__.py +2 -0
  5. ripperdoc/cli/commands/agents_cmd.py +12 -9
  6. ripperdoc/cli/commands/compact_cmd.py +7 -3
  7. ripperdoc/cli/commands/context_cmd.py +35 -15
  8. ripperdoc/cli/commands/doctor_cmd.py +27 -14
  9. ripperdoc/cli/commands/exit_cmd.py +1 -1
  10. ripperdoc/cli/commands/mcp_cmd.py +13 -8
  11. ripperdoc/cli/commands/memory_cmd.py +5 -5
  12. ripperdoc/cli/commands/models_cmd.py +47 -16
  13. ripperdoc/cli/commands/permissions_cmd.py +302 -0
  14. ripperdoc/cli/commands/resume_cmd.py +1 -2
  15. ripperdoc/cli/commands/tasks_cmd.py +24 -13
  16. ripperdoc/cli/ui/rich_ui.py +523 -396
  17. ripperdoc/cli/ui/tool_renderers.py +298 -0
  18. ripperdoc/core/agents.py +172 -4
  19. ripperdoc/core/config.py +130 -6
  20. ripperdoc/core/default_tools.py +13 -2
  21. ripperdoc/core/permissions.py +20 -14
  22. ripperdoc/core/providers/__init__.py +31 -15
  23. ripperdoc/core/providers/anthropic.py +122 -8
  24. ripperdoc/core/providers/base.py +93 -15
  25. ripperdoc/core/providers/gemini.py +539 -96
  26. ripperdoc/core/providers/openai.py +371 -26
  27. ripperdoc/core/query.py +301 -62
  28. ripperdoc/core/query_utils.py +51 -7
  29. ripperdoc/core/skills.py +295 -0
  30. ripperdoc/core/system_prompt.py +79 -67
  31. ripperdoc/core/tool.py +15 -6
  32. ripperdoc/sdk/client.py +14 -1
  33. ripperdoc/tools/ask_user_question_tool.py +431 -0
  34. ripperdoc/tools/background_shell.py +82 -26
  35. ripperdoc/tools/bash_tool.py +356 -209
  36. ripperdoc/tools/dynamic_mcp_tool.py +428 -0
  37. ripperdoc/tools/enter_plan_mode_tool.py +226 -0
  38. ripperdoc/tools/exit_plan_mode_tool.py +153 -0
  39. ripperdoc/tools/file_edit_tool.py +53 -10
  40. ripperdoc/tools/file_read_tool.py +17 -7
  41. ripperdoc/tools/file_write_tool.py +49 -13
  42. ripperdoc/tools/glob_tool.py +10 -9
  43. ripperdoc/tools/grep_tool.py +182 -51
  44. ripperdoc/tools/ls_tool.py +6 -6
  45. ripperdoc/tools/mcp_tools.py +172 -413
  46. ripperdoc/tools/multi_edit_tool.py +49 -9
  47. ripperdoc/tools/notebook_edit_tool.py +57 -13
  48. ripperdoc/tools/skill_tool.py +205 -0
  49. ripperdoc/tools/task_tool.py +91 -9
  50. ripperdoc/tools/todo_tool.py +12 -12
  51. ripperdoc/tools/tool_search_tool.py +5 -6
  52. ripperdoc/utils/coerce.py +34 -0
  53. ripperdoc/utils/context_length_errors.py +252 -0
  54. ripperdoc/utils/file_watch.py +5 -4
  55. ripperdoc/utils/json_utils.py +4 -4
  56. ripperdoc/utils/log.py +3 -3
  57. ripperdoc/utils/mcp.py +82 -22
  58. ripperdoc/utils/memory.py +9 -6
  59. ripperdoc/utils/message_compaction.py +19 -16
  60. ripperdoc/utils/messages.py +73 -8
  61. ripperdoc/utils/path_ignore.py +677 -0
  62. ripperdoc/utils/permissions/__init__.py +7 -1
  63. ripperdoc/utils/permissions/path_validation_utils.py +5 -3
  64. ripperdoc/utils/permissions/shell_command_validation.py +496 -18
  65. ripperdoc/utils/prompt.py +1 -1
  66. ripperdoc/utils/safe_get_cwd.py +5 -2
  67. ripperdoc/utils/session_history.py +38 -19
  68. ripperdoc/utils/todo.py +6 -2
  69. ripperdoc/utils/token_estimation.py +34 -0
  70. {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/METADATA +14 -1
  71. ripperdoc-0.2.5.dist-info/RECORD +107 -0
  72. ripperdoc-0.2.3.dist-info/RECORD +0 -95
  73. {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/WHEEL +0 -0
  74. {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/entry_points.txt +0 -0
  75. {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/licenses/LICENSE +0 -0
  76. {ripperdoc-0.2.3.dist-info → ripperdoc-0.2.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,298 @@
1
+ """Tool result renderers for CLI display.
2
+
3
+ This module provides a strategy pattern implementation for rendering different
4
+ tool results in the Rich CLI interface.
5
+ """
6
+
7
+ from typing import Any, Callable, List, Optional
8
+
9
+ from rich.console import Console
10
+ from rich.markup import escape
11
+
12
+
13
+ class ToolResultRenderer:
14
+ """Base class for rendering tool results to console."""
15
+
16
+ def __init__(self, console: Console, verbose: bool = False):
17
+ self.console = console
18
+ self.verbose = verbose
19
+
20
+ def can_handle(self, _sender: str) -> bool:
21
+ """Return True if this renderer handles the given tool name."""
22
+ raise NotImplementedError
23
+
24
+ def render(self, _content: str, _tool_data: Any) -> None:
25
+ """Render the tool result to console."""
26
+ raise NotImplementedError
27
+
28
+ def _get_field(self, data: Any, key: str, default: Any = None) -> Any:
29
+ """Safely fetch a field from either an object or a dict."""
30
+ if isinstance(data, dict):
31
+ return data.get(key, default)
32
+ return getattr(data, key, default)
33
+
34
+
35
+ class TodoResultRenderer(ToolResultRenderer):
36
+ """Render Todo tool results."""
37
+
38
+ def can_handle(self, sender: str) -> bool:
39
+ return "Todo" in sender
40
+
41
+ def render(self, content: str, _tool_data: Any) -> None:
42
+ lines = content.splitlines()
43
+ if lines:
44
+ self.console.print(f" ⎿ [dim]{escape(lines[0])}[/]")
45
+ for line in lines[1:]:
46
+ self.console.print(f" {line}", markup=False)
47
+ else:
48
+ self.console.print(" ⎿ [dim]Todo update[/]")
49
+
50
+
51
+ class ReadResultRenderer(ToolResultRenderer):
52
+ """Render Read/View tool results."""
53
+
54
+ def can_handle(self, sender: str) -> bool:
55
+ return "Read" in sender or "View" in sender
56
+
57
+ def render(self, content: str, _tool_data: Any) -> None:
58
+ lines = content.split("\n")
59
+ line_count = len(lines)
60
+ self.console.print(f" ⎿ [dim]Read {line_count} lines[/]")
61
+ if self.verbose:
62
+ preview = lines[:30]
63
+ for line in preview:
64
+ self.console.print(line, markup=False)
65
+ if len(lines) > len(preview):
66
+ self.console.print(f"[dim]... ({len(lines) - len(preview)} more lines)[/]")
67
+
68
+
69
+ class EditResultRenderer(ToolResultRenderer):
70
+ """Render Write/Edit/MultiEdit tool results."""
71
+
72
+ def can_handle(self, sender: str) -> bool:
73
+ return "Write" in sender or "Edit" in sender or "MultiEdit" in sender
74
+
75
+ def render(self, _content: str, tool_data: Any) -> None:
76
+ if tool_data and (hasattr(tool_data, "file_path") or isinstance(tool_data, dict)):
77
+ file_path = self._get_field(tool_data, "file_path")
78
+ additions = self._get_field(tool_data, "additions", 0)
79
+ deletions = self._get_field(tool_data, "deletions", 0)
80
+ diff_with_line_numbers = self._get_field(tool_data, "diff_with_line_numbers", [])
81
+
82
+ if not file_path:
83
+ self.console.print(" ⎿ [dim]File updated successfully[/]")
84
+ return
85
+
86
+ self.console.print(
87
+ f" ⎿ [dim]Updated {escape(str(file_path))} with {additions} additions and {deletions} removals[/]"
88
+ )
89
+
90
+ if self.verbose:
91
+ for line in diff_with_line_numbers:
92
+ self.console.print(line, markup=False)
93
+ else:
94
+ self.console.print(" ⎿ [dim]File updated successfully[/]")
95
+
96
+
97
+ class GlobResultRenderer(ToolResultRenderer):
98
+ """Render Glob tool results."""
99
+
100
+ def can_handle(self, sender: str) -> bool:
101
+ return "Glob" in sender
102
+
103
+ def render(self, content: str, _tool_data: Any) -> None:
104
+ files = content.split("\n")
105
+ file_count = len([f for f in files if f.strip()])
106
+ self.console.print(f" ⎿ [dim]Found {file_count} files[/]")
107
+ if self.verbose:
108
+ for line in files[:30]:
109
+ if line.strip():
110
+ self.console.print(f" {line}", markup=False)
111
+ if file_count > 30:
112
+ self.console.print(f"[dim]... ({file_count - 30} more)[/]")
113
+
114
+
115
+ class GrepResultRenderer(ToolResultRenderer):
116
+ """Render Grep tool results."""
117
+
118
+ def can_handle(self, sender: str) -> bool:
119
+ return "Grep" in sender
120
+
121
+ def render(self, content: str, _tool_data: Any) -> None:
122
+ matches = content.split("\n")
123
+ match_count = len([m for m in matches if m.strip()])
124
+ self.console.print(f" ⎿ [dim]Found {match_count} matches[/]")
125
+ if self.verbose:
126
+ for line in matches[:30]:
127
+ if line.strip():
128
+ self.console.print(f" {line}", markup=False)
129
+ if match_count > 30:
130
+ self.console.print(f"[dim]... ({match_count - 30} more)[/]")
131
+
132
+
133
+ class LSResultRenderer(ToolResultRenderer):
134
+ """Render LS tool results."""
135
+
136
+ def can_handle(self, sender: str) -> bool:
137
+ return "LS" in sender
138
+
139
+ def render(self, content: str, _tool_data: Any) -> None:
140
+ tree_lines = content.splitlines()
141
+ self.console.print(f" ⎿ [dim]Directory tree ({len(tree_lines)} lines)[/]")
142
+ if self.verbose:
143
+ preview = tree_lines[:40]
144
+ for line in preview:
145
+ self.console.print(f" {line}", markup=False)
146
+ if len(tree_lines) > len(preview):
147
+ self.console.print(f"[dim]... ({len(tree_lines) - len(preview)} more)[/]")
148
+
149
+
150
+ # Type alias for bash output parser callback
151
+ BashOutputParser = Callable[[str], tuple[List[str], List[str]]]
152
+
153
+
154
+ class BashResultRenderer(ToolResultRenderer):
155
+ """Render Bash tool results."""
156
+
157
+ def __init__(
158
+ self, console: Console, verbose: bool = False, parse_fallback: Optional[BashOutputParser] = None
159
+ ):
160
+ super().__init__(console, verbose)
161
+ self._parse_fallback = parse_fallback
162
+
163
+ def can_handle(self, sender: str) -> bool:
164
+ return "Bash" in sender
165
+
166
+ def render(self, content: str, tool_data: Any) -> None:
167
+ stdout_lines: List[str] = []
168
+ stderr_lines: List[str] = []
169
+ exit_code = 0
170
+ duration_ms = 0
171
+ timeout_ms = 0
172
+
173
+ if tool_data:
174
+ exit_code = self._get_field(tool_data, "exit_code", 0)
175
+ stdout = self._get_field(tool_data, "stdout", "") or ""
176
+ stderr = self._get_field(tool_data, "stderr", "") or ""
177
+ duration_ms = self._get_field(tool_data, "duration_ms", 0) or 0
178
+ timeout_ms = self._get_field(tool_data, "timeout_ms", 0) or 0
179
+ stdout_lines = stdout.splitlines() if stdout else []
180
+ stderr_lines = stderr.splitlines() if stderr else []
181
+
182
+ if not stdout_lines and not stderr_lines and content and self._parse_fallback:
183
+ stdout_lines, stderr_lines = self._parse_fallback(content)
184
+
185
+ show_inline_stdout = (
186
+ stdout_lines and not stderr_lines and exit_code == 0 and not self.verbose
187
+ )
188
+
189
+ if show_inline_stdout:
190
+ preview = stdout_lines if self.verbose else stdout_lines[:5]
191
+ self.console.print(f" ⎿ {preview[0]}", markup=False)
192
+ for line in preview[1:]:
193
+ self.console.print(f" {line}", markup=False)
194
+ if not self.verbose and len(stdout_lines) > len(preview):
195
+ self.console.print(f"[dim]... ({len(stdout_lines) - len(preview)} more lines)[/]")
196
+ else:
197
+ self._render_detailed_output(
198
+ tool_data, exit_code, duration_ms, timeout_ms, stdout_lines, stderr_lines
199
+ )
200
+
201
+ def _render_detailed_output(
202
+ self,
203
+ tool_data: Any,
204
+ exit_code: int,
205
+ duration_ms: float,
206
+ timeout_ms: int,
207
+ stdout_lines: List[str],
208
+ stderr_lines: List[str],
209
+ ) -> None:
210
+ """Render detailed Bash output with exit code, stdout, stderr."""
211
+ if tool_data:
212
+ timing = ""
213
+ if duration_ms:
214
+ timing = f" ({duration_ms / 1000:.2f}s"
215
+ if timeout_ms:
216
+ timing += f" / timeout {timeout_ms / 1000:.0f}s"
217
+ timing += ")"
218
+ elif timeout_ms:
219
+ timing = f" (timeout {timeout_ms / 1000:.0f}s)"
220
+ self.console.print(f" ⎿ [dim]Exit code {exit_code}{timing}[/]")
221
+ else:
222
+ self.console.print(" ⎿ [dim]Command executed[/]")
223
+
224
+ # Render stdout
225
+ if stdout_lines:
226
+ preview = stdout_lines if self.verbose else stdout_lines[:5]
227
+ self.console.print("[dim]stdout:[/]")
228
+ for line in preview:
229
+ self.console.print(f" {line}", markup=False)
230
+ if not self.verbose and len(stdout_lines) > len(preview):
231
+ self.console.print(
232
+ f"[dim]... ({len(stdout_lines) - len(preview)} more stdout lines)[/]"
233
+ )
234
+ else:
235
+ self.console.print("[dim]stdout:[/]")
236
+ self.console.print(" [dim](no stdout)[/]")
237
+
238
+ # Render stderr
239
+ if stderr_lines:
240
+ preview = stderr_lines if self.verbose else stderr_lines[:5]
241
+ self.console.print("[dim]stderr:[/]")
242
+ for line in preview:
243
+ self.console.print(f" {line}", markup=False)
244
+ if not self.verbose and len(stderr_lines) > len(preview):
245
+ self.console.print(
246
+ f"[dim]... ({len(stderr_lines) - len(preview)} more stderr lines)[/]"
247
+ )
248
+ else:
249
+ self.console.print("[dim]stderr:[/]")
250
+ self.console.print(" [dim](no stderr)[/]")
251
+
252
+
253
+ class ToolResultRendererRegistry:
254
+ """Registry that selects the appropriate renderer for a tool result."""
255
+
256
+ def __init__(
257
+ self, console: Console, verbose: bool = False, parse_bash_fallback: Optional[BashOutputParser] = None
258
+ ):
259
+ self.console = console
260
+ self.verbose = verbose
261
+ self._renderers: List[ToolResultRenderer] = [
262
+ TodoResultRenderer(console, verbose),
263
+ ReadResultRenderer(console, verbose),
264
+ EditResultRenderer(console, verbose),
265
+ GlobResultRenderer(console, verbose),
266
+ GrepResultRenderer(console, verbose),
267
+ LSResultRenderer(console, verbose),
268
+ BashResultRenderer(console, verbose, parse_bash_fallback),
269
+ ]
270
+
271
+ def get_renderer(self, sender: str) -> Optional[ToolResultRenderer]:
272
+ """Get the appropriate renderer for the given tool name."""
273
+ for renderer in self._renderers:
274
+ if renderer.can_handle(sender):
275
+ return renderer
276
+ return None
277
+
278
+ def render(self, sender: str, content: str, tool_data: Any) -> bool:
279
+ """Render the tool result. Returns True if rendered, False otherwise."""
280
+ renderer = self.get_renderer(sender)
281
+ if renderer:
282
+ renderer.render(content, tool_data)
283
+ return True
284
+ return False
285
+
286
+
287
+ __all__ = [
288
+ "ToolResultRenderer",
289
+ "ToolResultRendererRegistry",
290
+ "TodoResultRenderer",
291
+ "ReadResultRenderer",
292
+ "EditResultRenderer",
293
+ "GlobResultRenderer",
294
+ "GrepResultRenderer",
295
+ "LSResultRenderer",
296
+ "BashResultRenderer",
297
+ "BashOutputParser",
298
+ ]
ripperdoc/core/agents.py CHANGED
@@ -11,11 +11,63 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple
11
11
  import yaml
12
12
 
13
13
  from ripperdoc.utils.log import get_logger
14
+ from ripperdoc.tools.ask_user_question_tool import AskUserQuestionTool
15
+ from ripperdoc.tools.bash_output_tool import BashOutputTool
16
+ from ripperdoc.tools.bash_tool import BashTool
17
+ from ripperdoc.tools.enter_plan_mode_tool import EnterPlanModeTool
18
+ from ripperdoc.tools.exit_plan_mode_tool import ExitPlanModeTool
19
+ from ripperdoc.tools.file_edit_tool import FileEditTool
20
+ from ripperdoc.tools.file_read_tool import FileReadTool
21
+ from ripperdoc.tools.file_write_tool import FileWriteTool
22
+ from ripperdoc.tools.glob_tool import GlobTool
23
+ from ripperdoc.tools.grep_tool import GrepTool
24
+ from ripperdoc.tools.kill_bash_tool import KillBashTool
25
+ from ripperdoc.tools.ls_tool import LSTool
26
+ from ripperdoc.tools.multi_edit_tool import MultiEditTool
27
+ from ripperdoc.tools.notebook_edit_tool import NotebookEditTool
28
+ from ripperdoc.tools.todo_tool import TodoReadTool, TodoWriteTool
29
+ from ripperdoc.tools.tool_search_tool import ToolSearchTool
30
+ from ripperdoc.tools.mcp_tools import (
31
+ ListMcpResourcesTool,
32
+ ListMcpServersTool,
33
+ ReadMcpResourceTool,
34
+ )
14
35
 
15
36
 
16
37
  logger = get_logger()
17
38
 
18
39
 
40
+ def _safe_tool_name(factory: Any, fallback: str) -> str:
41
+ try:
42
+ name = getattr(factory(), "name", None)
43
+ return str(name) if name else fallback
44
+ except (TypeError, ValueError, RuntimeError, AttributeError):
45
+ return fallback
46
+
47
+
48
+ GLOB_TOOL_NAME = _safe_tool_name(GlobTool, "Glob")
49
+ GREP_TOOL_NAME = _safe_tool_name(GrepTool, "Grep")
50
+ VIEW_TOOL_NAME = _safe_tool_name(FileReadTool, "View")
51
+ FILE_EDIT_TOOL_NAME = _safe_tool_name(FileEditTool, "FileEdit")
52
+ MULTI_EDIT_TOOL_NAME = _safe_tool_name(MultiEditTool, "MultiEdit")
53
+ NOTEBOOK_EDIT_TOOL_NAME = _safe_tool_name(NotebookEditTool, "NotebookEdit")
54
+ FILE_WRITE_TOOL_NAME = _safe_tool_name(FileWriteTool, "FileWrite")
55
+ LS_TOOL_NAME = _safe_tool_name(LSTool, "LS")
56
+ BASH_TOOL_NAME = _safe_tool_name(BashTool, "Bash")
57
+ BASH_OUTPUT_TOOL_NAME = _safe_tool_name(BashOutputTool, "BashOutput")
58
+ KILL_BASH_TOOL_NAME = _safe_tool_name(KillBashTool, "KillBash")
59
+ TODO_READ_TOOL_NAME = _safe_tool_name(TodoReadTool, "TodoRead")
60
+ TODO_WRITE_TOOL_NAME = _safe_tool_name(TodoWriteTool, "TodoWrite")
61
+ ASK_USER_QUESTION_TOOL_NAME = _safe_tool_name(AskUserQuestionTool, "AskUserQuestion")
62
+ ENTER_PLAN_MODE_TOOL_NAME = _safe_tool_name(EnterPlanModeTool, "EnterPlanMode")
63
+ EXIT_PLAN_MODE_TOOL_NAME = _safe_tool_name(ExitPlanModeTool, "ExitPlanMode")
64
+ TOOL_SEARCH_TOOL_NAME = _safe_tool_name(ToolSearchTool, "ToolSearch")
65
+ MCP_LIST_SERVERS_TOOL_NAME = _safe_tool_name(ListMcpServersTool, "ListMcpServers")
66
+ MCP_LIST_RESOURCES_TOOL_NAME = _safe_tool_name(ListMcpResourcesTool, "ListMcpResources")
67
+ MCP_READ_RESOURCE_TOOL_NAME = _safe_tool_name(ReadMcpResourceTool, "ReadMcpResource")
68
+ TASK_TOOL_NAME = "Task"
69
+
70
+
19
71
  AGENT_DIR_NAME = "agents"
20
72
 
21
73
 
@@ -64,6 +116,86 @@ CODE_REVIEW_AGENT_PROMPT = (
64
116
  "Provide clear, actionable feedback that the parent agent can relay to the user."
65
117
  )
66
118
 
119
+ EXPLORE_AGENT_PROMPT = (
120
+ "You are a file search specialist. "
121
+ "You excel at thoroughly navigating and exploring codebases.\n\n"
122
+ "=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===\n"
123
+ "This is a READ-ONLY exploration task. You are STRICTLY PROHIBITED from:\n"
124
+ "- Creating new files (no Write, touch, or file creation of any kind)\n"
125
+ "- Modifying existing files (no Edit operations)\n"
126
+ "- Deleting files (no rm or deletion)\n"
127
+ "- Moving or copying files (no mv or cp)\n"
128
+ "- Creating temporary files anywhere, including /tmp\n"
129
+ "- Using redirect operators (>, >>, |) or heredocs to write to files\n"
130
+ "- Running ANY commands that change system state\n\n"
131
+ "Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access "
132
+ "to file editing tools - attempting to edit files will fail.\n\n"
133
+ "Your strengths:\n"
134
+ "- Rapidly finding files using glob patterns\n"
135
+ "- Searching code and text with powerful regex patterns\n"
136
+ "- Reading and analyzing file contents\n\n"
137
+ "Guidelines:\n"
138
+ f"- Use {GLOB_TOOL_NAME} for broad file pattern matching\n"
139
+ f"- Use {GREP_TOOL_NAME} for searching file contents with regex\n"
140
+ f"- Use {VIEW_TOOL_NAME} when you know the specific file path you need to read\n"
141
+ f"- Use {BASH_TOOL_NAME} ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)\n"
142
+ f"- NEVER use {BASH_TOOL_NAME} for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification\n"
143
+ "- Adapt your search approach based on the thoroughness level specified by the caller\n"
144
+ "- Return file paths as absolute paths in your final response\n"
145
+ "- For clear communication, avoid using emojis\n"
146
+ "- Communicate your final report directly as a regular message - do NOT attempt to create files\n\n"
147
+ "NOTE: You are meant to be a fast agent that returns output as quickly as possible. In order to achieve this you must:\n"
148
+ "- Make efficient use of the tools that you have at your disposal: be smart about how you search for files and implementations\n"
149
+ "- Wherever possible you should try to spawn multiple parallel tool calls for grepping and reading files\n\n"
150
+ "Complete the user's search request efficiently and report your findings clearly."
151
+ )
152
+
153
+ PLAN_AGENT_PROMPT = (
154
+ "You are a software architect and planning specialist. Your role is "
155
+ "to explore the codebase and design implementation plans.\n\n"
156
+ "=== CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS ===\n"
157
+ "This is a READ-ONLY planning task. You are STRICTLY PROHIBITED from:\n"
158
+ "- Creating new files (no Write, touch, or file creation of any kind)\n"
159
+ "- Modifying existing files (no Edit operations)\n"
160
+ "- Deleting files (no rm or deletion)\n"
161
+ "- Moving or copying files (no mv or cp)\n"
162
+ "- Creating temporary files anywhere, including /tmp\n"
163
+ "- Using redirect operators (>, >>, |) or heredocs to write to files\n"
164
+ "- Running ANY commands that change system state\n\n"
165
+ "Your role is EXCLUSIVELY to explore the codebase and design implementation plans. "
166
+ "You do NOT have access to file editing tools - attempting to edit files will fail.\n\n"
167
+ "You will be provided with a set of requirements and optionally a perspective on how "
168
+ "to approach the design process.\n\n"
169
+ "## Your Process\n\n"
170
+ "1. **Understand Requirements**: Focus on the requirements provided and apply your "
171
+ "assigned perspective throughout the design process.\n\n"
172
+ "2. **Explore Thoroughly**:\n"
173
+ " - Read any files provided to you in the initial prompt\n"
174
+ f" - Find existing patterns and conventions using {GLOB_TOOL_NAME}, {GREP_TOOL_NAME}, and {VIEW_TOOL_NAME}\n"
175
+ " - Understand the current architecture\n"
176
+ " - Identify similar features as reference\n"
177
+ " - Trace through relevant code paths\n"
178
+ f" - Use {BASH_TOOL_NAME} ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)\n"
179
+ f" - NEVER use {BASH_TOOL_NAME} for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, or any file creation/modification\n\n"
180
+ "3. **Design Solution**:\n"
181
+ " - Create implementation approach based on your assigned perspective\n"
182
+ " - Consider trade-offs and architectural decisions\n"
183
+ " - Follow existing patterns where appropriate\n\n"
184
+ "4. **Detail the Plan**:\n"
185
+ " - Provide step-by-step implementation strategy\n"
186
+ " - Identify dependencies and sequencing\n"
187
+ " - Anticipate potential challenges\n\n"
188
+ "## Required Output\n\n"
189
+ "End your response with:\n\n"
190
+ "### Critical Files for Implementation\n"
191
+ "List 3-5 files most critical for implementing this plan:\n"
192
+ '- path/to/file1.ts - [Brief reason: e.g., "Core logic to modify"]\n'
193
+ '- path/to/file2.ts - [Brief reason: e.g., "Interfaces to implement"]\n'
194
+ '- path/to/file3.ts - [Brief reason: e.g., "Pattern to follow"]\n\n'
195
+ "REMEMBER: You can ONLY explore and plan. You CANNOT and MUST NOT write, edit, or "
196
+ "modify any files. You do NOT have access to file editing tools."
197
+ )
198
+
67
199
 
68
200
  def _built_in_agents() -> List[AgentDefinition]:
69
201
  return [
@@ -89,6 +221,34 @@ def _built_in_agents() -> List[AgentDefinition]:
89
221
  location=AgentLocation.BUILT_IN,
90
222
  color="yellow",
91
223
  ),
224
+ AgentDefinition(
225
+ agent_type="explore",
226
+ when_to_use=(
227
+ "Fast agent specialized for exploring codebases. Use this when you need to quickly find "
228
+ 'files by patterns (eg. "src/components/**/*.tsx"), search code for keywords (eg. "API endpoints"), '
229
+ 'or answer questions about the codebase (eg. "how do API endpoints work?"). When calling this agent, '
230
+ 'specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, '
231
+ 'or "very thorough" for comprehensive analysis across multiple locations and naming conventions.'
232
+ ),
233
+ tools=["View", "Glob", "Grep"],
234
+ system_prompt=EXPLORE_AGENT_PROMPT,
235
+ location=AgentLocation.BUILT_IN,
236
+ color="green",
237
+ model="task",
238
+ ),
239
+ AgentDefinition(
240
+ agent_type="plan",
241
+ when_to_use=(
242
+ "Software architect agent for designing implementation plans. Use this when "
243
+ "you need to plan the implementation strategy for a task. Returns step-by-step "
244
+ "plans, identifies critical files, and considers architectural trade-offs."
245
+ ),
246
+ tools=["View", "Glob", "Grep"],
247
+ system_prompt=PLAN_AGENT_PROMPT,
248
+ location=AgentLocation.BUILT_IN,
249
+ color="blue",
250
+ model=None,
251
+ ),
92
252
  ]
93
253
 
94
254
 
@@ -118,8 +278,12 @@ def _split_frontmatter(raw_text: str) -> Tuple[Dict[str, Any], str]:
118
278
  body = "\n".join(lines[idx + 1 :])
119
279
  try:
120
280
  frontmatter = yaml.safe_load(frontmatter_text) or {}
121
- except Exception as exc: # pragma: no cover - defensive
122
- logger.exception("Invalid frontmatter in agent file", extra={"error": str(exc)})
281
+ except (yaml.YAMLError, ValueError, TypeError) as exc: # pragma: no cover - defensive
282
+ logger.warning(
283
+ "Invalid frontmatter in agent file: %s: %s",
284
+ type(exc).__name__, exc,
285
+ extra={"error": str(exc)},
286
+ )
123
287
  return {"__error__": f"Invalid frontmatter: {exc}"}, body
124
288
  return frontmatter, body
125
289
  return {}, raw_text
@@ -145,8 +309,12 @@ def _parse_agent_file(
145
309
  """Parse a single agent file."""
146
310
  try:
147
311
  text = path.read_text(encoding="utf-8")
148
- except Exception as exc:
149
- logger.exception("Failed to read agent file", extra={"error": str(exc), "path": str(path)})
312
+ except (OSError, IOError, UnicodeDecodeError) as exc:
313
+ logger.warning(
314
+ "Failed to read agent file: %s: %s",
315
+ type(exc).__name__, exc,
316
+ extra={"error": str(exc), "path": str(path)},
317
+ )
150
318
  return None, f"Failed to read agent file {path}: {exc}"
151
319
 
152
320
  frontmatter, body = _split_frontmatter(text)