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
@@ -7,7 +7,7 @@ import re
7
7
  from typing import Any, Dict, List, Mapping, Optional, Union
8
8
  from uuid import uuid4
9
9
 
10
- from json_repair import repair_json
10
+ from json_repair import repair_json # type: ignore[import-not-found]
11
11
  from pydantic import ValidationError
12
12
 
13
13
  from ripperdoc.core.config import ModelProfile, ProviderType, get_global_config
@@ -66,16 +66,43 @@ def anthropic_usage_tokens(usage: Optional[Mapping[str, Any] | object]) -> Dict[
66
66
  def openai_usage_tokens(usage: Optional[Mapping[str, Any] | object]) -> Dict[str, int]:
67
67
  """Extract token counts from an OpenAI-compatible response usage payload."""
68
68
  prompt_details = None
69
+ input_details = None
70
+ output_details = None
69
71
  if isinstance(usage, dict):
70
72
  prompt_details = usage.get("prompt_tokens_details")
73
+ input_details = usage.get("input_tokens_details")
74
+ output_details = usage.get("output_tokens_details")
71
75
  else:
72
76
  prompt_details = getattr(usage, "prompt_tokens_details", None)
73
-
74
- cache_read_tokens = _get_usage_field(prompt_details, "cached_tokens") if prompt_details else 0
77
+ input_details = getattr(usage, "input_tokens_details", None)
78
+ output_details = getattr(usage, "output_tokens_details", None)
79
+
80
+ cache_read_tokens = 0
81
+ if prompt_details:
82
+ cache_read_tokens = _get_usage_field(prompt_details, "cached_tokens")
83
+ if not cache_read_tokens and input_details:
84
+ cache_read_tokens = _get_usage_field(input_details, "cached_tokens")
85
+
86
+ input_tokens = _get_usage_field(usage, "prompt_tokens")
87
+ if not input_tokens:
88
+ input_tokens = _get_usage_field(usage, "input_tokens")
89
+
90
+ output_tokens = _get_usage_field(usage, "completion_tokens")
91
+ if not output_tokens:
92
+ output_tokens = _get_usage_field(usage, "output_tokens")
93
+
94
+ reasoning_tokens = _get_usage_field(output_details, "reasoning_tokens") if output_details else 0
95
+ if reasoning_tokens:
96
+ if output_tokens <= 0:
97
+ output_tokens = reasoning_tokens
98
+ elif output_tokens < reasoning_tokens:
99
+ output_tokens = output_tokens + reasoning_tokens
100
+ else:
101
+ output_tokens = max(output_tokens, reasoning_tokens)
75
102
 
76
103
  return {
77
- "input_tokens": _get_usage_field(usage, "prompt_tokens"),
78
- "output_tokens": _get_usage_field(usage, "completion_tokens"),
104
+ "input_tokens": input_tokens,
105
+ "output_tokens": output_tokens,
79
106
  "cache_read_input_tokens": cache_read_tokens,
80
107
  "cache_creation_input_tokens": 0,
81
108
  }
@@ -219,10 +246,10 @@ def _tool_prompt_for_text_mode(tools: List[Tool[Any, Any]]) -> str:
219
246
  if hasattr(finfo, "is_required"):
220
247
  try:
221
248
  is_req = bool(finfo.is_required())
222
- except Exception:
249
+ except (TypeError, AttributeError):
223
250
  is_req = False
224
251
  required_fields.append(f"{fname}{' (required)' if is_req else ''}")
225
- except Exception:
252
+ except (AttributeError, TypeError):
226
253
  required_fields = []
227
254
 
228
255
  required_str = ", ".join(required_fields) if required_fields else "see input schema"
@@ -487,6 +514,23 @@ def content_blocks_from_anthropic_response(response: Any, tool_mode: str) -> Lis
487
514
  btype = getattr(block, "type", None)
488
515
  if btype == "text":
489
516
  blocks.append({"type": "text", "text": getattr(block, "text", "")})
517
+ elif btype == "thinking":
518
+ blocks.append(
519
+ {
520
+ "type": "thinking",
521
+ "thinking": getattr(block, "thinking", None) or "",
522
+ "signature": getattr(block, "signature", None),
523
+ }
524
+ )
525
+ elif btype == "redacted_thinking":
526
+ # Preserve encrypted payload for replay even if we don't display it.
527
+ blocks.append(
528
+ {
529
+ "type": "redacted_thinking",
530
+ "data": getattr(block, "data", None),
531
+ "signature": getattr(block, "signature", None),
532
+ }
533
+ )
490
534
  elif btype == "tool_use":
491
535
  raw_input = getattr(block, "input", {}) or {}
492
536
  blocks.append(
@@ -0,0 +1,295 @@
1
+ """Agent Skill loading helpers for Ripperdoc.
2
+
3
+ Skills are small capability bundles defined by SKILL.md files that live under
4
+ `~/.ripperdoc/skills` or `.ripperdoc/skills` in a project. Only the skill
5
+ metadata (name + description) should be added to the system prompt up front;
6
+ the full content is loaded on demand via the Skill tool. Optional frontmatter
7
+ fields include:
8
+ - allowed-tools: Comma-separated list of tools that are allowed/preferred.
9
+ - model: Model pointer hint for this skill.
10
+ - max-thinking-tokens: Reasoning budget hint for this skill.
11
+ - disable-model-invocation: If true, block the Skill tool from loading this
12
+ skill.
13
+ - type: Skill kind (defaults to "prompt").
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import re
19
+ from dataclasses import dataclass
20
+ from enum import Enum
21
+ from pathlib import Path
22
+ from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple
23
+
24
+ import yaml
25
+
26
+ from ripperdoc.utils.coerce import parse_boolish, parse_optional_int
27
+ from ripperdoc.utils.log import get_logger
28
+
29
+ logger = get_logger()
30
+
31
+ SKILL_DIR_NAME = "skills"
32
+ SKILL_FILE_NAME = "SKILL.md"
33
+ _SKILL_NAME_RE = re.compile(r"^[a-z0-9-]{1,64}$")
34
+
35
+
36
+ class SkillLocation(str, Enum):
37
+ """Where a skill definition is sourced from."""
38
+
39
+ USER = "user"
40
+ PROJECT = "project"
41
+ OTHER = "other"
42
+
43
+
44
+ @dataclass
45
+ class SkillDefinition:
46
+ """Parsed representation of a skill."""
47
+
48
+ name: str
49
+ description: str
50
+ content: str
51
+ path: Path
52
+ base_dir: Path
53
+ location: SkillLocation
54
+ allowed_tools: List[str]
55
+ model: Optional[str] = None
56
+ max_thinking_tokens: Optional[int] = None
57
+ skill_type: str = "prompt"
58
+ disable_model_invocation: bool = False
59
+
60
+
61
+ @dataclass
62
+ class SkillLoadError:
63
+ """Error encountered while loading a skill file."""
64
+
65
+ path: Path
66
+ reason: str
67
+
68
+
69
+ @dataclass
70
+ class SkillLoadResult:
71
+ """Aggregated result of loading skills."""
72
+
73
+ skills: List[SkillDefinition]
74
+ errors: List[SkillLoadError]
75
+
76
+
77
+ def _split_frontmatter(raw_text: str) -> Tuple[Dict[str, Any], str]:
78
+ """Extract YAML frontmatter and body content from a markdown file."""
79
+ lines = raw_text.splitlines()
80
+ if len(lines) >= 3 and lines[0].strip() == "---":
81
+ for idx in range(1, len(lines)):
82
+ if lines[idx].strip() == "---":
83
+ frontmatter_text = "\n".join(lines[1:idx])
84
+ body = "\n".join(lines[idx + 1 :])
85
+ try:
86
+ frontmatter = yaml.safe_load(frontmatter_text) or {}
87
+ except (yaml.YAMLError, ValueError, TypeError) as exc: # pragma: no cover - defensive
88
+ logger.warning(
89
+ "[skills] Invalid frontmatter in SKILL.md: %s: %s",
90
+ type(exc).__name__, exc,
91
+ )
92
+ return {"__error__": f"Invalid frontmatter: {exc}"}, body
93
+ return frontmatter, body
94
+ return {}, raw_text
95
+
96
+
97
+ def _normalize_allowed_tools(value: object) -> List[str]:
98
+ """Normalize allowed-tools values to a clean list of tool names."""
99
+ if value is None:
100
+ return []
101
+ if isinstance(value, str):
102
+ return [item.strip() for item in value.split(",") if item.strip()]
103
+ if isinstance(value, Iterable):
104
+ tools: List[str] = []
105
+ for item in value:
106
+ if isinstance(item, str) and item.strip():
107
+ tools.append(item.strip())
108
+ return tools
109
+ return []
110
+
111
+
112
+ def _load_skill_file(
113
+ path: Path, location: SkillLocation
114
+ ) -> Tuple[Optional[SkillDefinition], Optional[SkillLoadError]]:
115
+ """Parse a single SKILL.md file."""
116
+ try:
117
+ text = path.read_text(encoding="utf-8")
118
+ except (OSError, IOError, UnicodeDecodeError) as exc:
119
+ logger.warning(
120
+ "[skills] Failed to read skill file: %s: %s",
121
+ type(exc).__name__, exc,
122
+ extra={"path": str(path)},
123
+ )
124
+ return None, SkillLoadError(path=path, reason=f"Failed to read file: {exc}")
125
+
126
+ frontmatter, body = _split_frontmatter(text)
127
+ if "__error__" in frontmatter:
128
+ return None, SkillLoadError(path=path, reason=str(frontmatter["__error__"]))
129
+
130
+ raw_name = frontmatter.get("name")
131
+ raw_description = frontmatter.get("description")
132
+ if not isinstance(raw_name, str) or not raw_name.strip():
133
+ return None, SkillLoadError(path=path, reason='Missing required "name" field')
134
+ if not _SKILL_NAME_RE.match(raw_name.strip()):
135
+ return None, SkillLoadError(
136
+ path=path,
137
+ reason='Invalid "name" format. Use lowercase letters, numbers, and hyphens only (max 64 chars).',
138
+ )
139
+ if not isinstance(raw_description, str) or not raw_description.strip():
140
+ return None, SkillLoadError(path=path, reason='Missing required "description" field')
141
+
142
+ allowed_tools = _normalize_allowed_tools(
143
+ frontmatter.get("allowed-tools") or frontmatter.get("allowed_tools")
144
+ )
145
+ model_value = frontmatter.get("model")
146
+ model = model_value if isinstance(model_value, str) and model_value.strip() else None
147
+ max_thinking_tokens = parse_optional_int(
148
+ frontmatter.get("max-thinking-tokens") or frontmatter.get("max_thinking_tokens")
149
+ )
150
+ raw_type = (
151
+ frontmatter.get("type")
152
+ or frontmatter.get("skill-type")
153
+ or frontmatter.get("skill_type")
154
+ or "prompt"
155
+ )
156
+ skill_type = str(raw_type).strip().lower() if isinstance(raw_type, str) else "prompt"
157
+ disable_model_invocation = parse_boolish(
158
+ frontmatter.get("disable-model-invocation") or frontmatter.get("disable_model_invocation")
159
+ )
160
+
161
+ skill = SkillDefinition(
162
+ name=raw_name.strip(),
163
+ description=raw_description.strip(),
164
+ content=body.strip(),
165
+ path=path,
166
+ base_dir=path.parent,
167
+ location=location,
168
+ allowed_tools=allowed_tools,
169
+ model=model,
170
+ max_thinking_tokens=max_thinking_tokens,
171
+ skill_type=skill_type or "prompt",
172
+ disable_model_invocation=disable_model_invocation,
173
+ )
174
+ return skill, None
175
+
176
+
177
+ def _load_skill_dir(
178
+ path: Path, location: SkillLocation
179
+ ) -> Tuple[List[SkillDefinition], List[SkillLoadError]]:
180
+ """Load skills from a directory that either contains SKILL.md or subdirectories."""
181
+ skills: List[SkillDefinition] = []
182
+ errors: List[SkillLoadError] = []
183
+ if not path.exists() or not path.is_dir():
184
+ return skills, errors
185
+
186
+ single_skill = path / SKILL_FILE_NAME
187
+ if single_skill.exists():
188
+ skill, error = _load_skill_file(single_skill, location)
189
+ if skill:
190
+ skills.append(skill)
191
+ elif error:
192
+ errors.append(error)
193
+ return skills, errors
194
+
195
+ for entry in sorted(path.iterdir()):
196
+ try:
197
+ if not entry.is_dir() and not entry.is_symlink():
198
+ continue
199
+ except OSError:
200
+ continue
201
+
202
+ candidate = entry / SKILL_FILE_NAME
203
+ if not candidate.exists():
204
+ continue
205
+ skill, error = _load_skill_file(candidate, location)
206
+ if skill:
207
+ skills.append(skill)
208
+ elif error:
209
+ errors.append(error)
210
+ return skills, errors
211
+
212
+
213
+ def skill_directories(
214
+ project_path: Optional[Path] = None, home: Optional[Path] = None
215
+ ) -> List[Tuple[Path, SkillLocation]]:
216
+ """Return the standard skill directories for user and project scopes."""
217
+ home_dir = (home or Path.home()).expanduser()
218
+ project_dir = (project_path or Path.cwd()).resolve()
219
+ return [
220
+ (home_dir / ".ripperdoc" / SKILL_DIR_NAME, SkillLocation.USER),
221
+ (project_dir / ".ripperdoc" / SKILL_DIR_NAME, SkillLocation.PROJECT),
222
+ ]
223
+
224
+
225
+ def load_all_skills(
226
+ project_path: Optional[Path] = None, home: Optional[Path] = None
227
+ ) -> SkillLoadResult:
228
+ """Load skills from user and project directories.
229
+
230
+ Project skills override user skills with the same name.
231
+ """
232
+ skills_by_name: Dict[str, SkillDefinition] = {}
233
+ errors: List[SkillLoadError] = []
234
+
235
+ # Load user first so project overrides take precedence.
236
+ for directory, location in skill_directories(project_path=project_path, home=home):
237
+ loaded, dir_errors = _load_skill_dir(directory, location)
238
+ errors.extend(dir_errors)
239
+ for skill in loaded:
240
+ if skill.name in skills_by_name:
241
+ logger.debug(
242
+ "[skills] Overriding skill",
243
+ extra={
244
+ "skill_name": skill.name,
245
+ "previous_location": str(skills_by_name[skill.name].location),
246
+ "new_location": str(location),
247
+ },
248
+ )
249
+ skills_by_name[skill.name] = skill
250
+ return SkillLoadResult(skills=list(skills_by_name.values()), errors=errors)
251
+
252
+
253
+ def find_skill(
254
+ skill_name: str, project_path: Optional[Path] = None, home: Optional[Path] = None
255
+ ) -> Optional[SkillDefinition]:
256
+ """Find a skill by name (case-sensitive match)."""
257
+ normalized = skill_name.strip().lstrip("/")
258
+ if not normalized:
259
+ return None
260
+ result = load_all_skills(project_path=project_path, home=home)
261
+ return next((skill for skill in result.skills if skill.name == normalized), None)
262
+
263
+
264
+ def build_skill_summary(skills: Sequence[SkillDefinition]) -> str:
265
+ """Render a concise instruction block listing available skills."""
266
+ if not skills:
267
+ return (
268
+ "# Skills\n"
269
+ "No skills detected. Add SKILL.md under ~/.ripperdoc/skills or ./.ripperdoc/skills "
270
+ "to extend capabilities, then load them with the Skill tool when relevant."
271
+ )
272
+ lines = [
273
+ "# Skills",
274
+ "Skills extend your capabilities with reusable instructions stored in SKILL.md files.",
275
+ 'Call the Skill tool with {"skill": "<name>"} to load a skill when it matches the user request.',
276
+ "Available skills:",
277
+ ]
278
+ for skill in skills:
279
+ location = f" ({skill.location.value})" if skill.location else ""
280
+ lines.append(f"- {skill.name}{location}: {skill.description}")
281
+ return "\n".join(lines)
282
+
283
+
284
+ __all__ = [
285
+ "SkillDefinition",
286
+ "SkillLoadError",
287
+ "SkillLoadResult",
288
+ "SkillLocation",
289
+ "SKILL_DIR_NAME",
290
+ "SKILL_FILE_NAME",
291
+ "load_all_skills",
292
+ "find_skill",
293
+ "build_skill_summary",
294
+ "skill_directories",
295
+ ]
@@ -8,7 +8,19 @@ from pathlib import Path
8
8
  from textwrap import dedent
9
9
  from typing import Any, Dict, Iterable, List, Optional
10
10
 
11
- from ripperdoc.core.agents import clear_agent_cache, load_agent_definitions, summarize_agent
11
+ from ripperdoc.core.agents import (
12
+ ASK_USER_QUESTION_TOOL_NAME,
13
+ BASH_TOOL_NAME,
14
+ FILE_EDIT_TOOL_NAME,
15
+ FILE_WRITE_TOOL_NAME,
16
+ TASK_TOOL_NAME,
17
+ TODO_WRITE_TOOL_NAME,
18
+ TOOL_SEARCH_TOOL_NAME,
19
+ VIEW_TOOL_NAME,
20
+ clear_agent_cache,
21
+ load_agent_definitions,
22
+ summarize_agent,
23
+ )
12
24
  from ripperdoc.core.tool import Tool
13
25
  from ripperdoc.utils.log import get_logger
14
26
 
@@ -20,7 +32,7 @@ DEFENSIVE_SECURITY_GUIDELINE = (
20
32
  "IMPORTANT: Assist with defensive security tasks only. Refuse to create, modify, or improve code that may be used maliciously. "
21
33
  "Allow security analysis, detection rules, vulnerability explanations, defensive tools, and security documentation."
22
34
  )
23
- FEEDBACK_URL = "https://github.com/jstzwj/Ripperdoc/issues"
35
+ FEEDBACK_URL = "https://github.com/quantmew/Ripperdoc/issues"
24
36
 
25
37
 
26
38
  def _detect_git_repo(cwd: Path) -> bool:
@@ -34,8 +46,12 @@ def _detect_git_repo(cwd: Path) -> bool:
34
46
  check=False,
35
47
  )
36
48
  return result.returncode == 0 and result.stdout.strip().lower() == "true"
37
- except Exception:
38
- logger.exception("[system_prompt] Failed to detect git repository", extra={"cwd": str(cwd)})
49
+ except (OSError, subprocess.SubprocessError) as exc:
50
+ logger.warning(
51
+ "[system_prompt] Failed to detect git repository: %s: %s",
52
+ type(exc).__name__, exc,
53
+ extra={"cwd": str(cwd)},
54
+ )
39
55
  return False
40
56
 
41
57
 
@@ -174,10 +190,18 @@ def build_system_prompt(
174
190
  ) -> str:
175
191
  _ = user_prompt, context
176
192
  tool_names = {tool.name for tool in tools}
177
- todo_tool_name = "TodoWrite"
193
+ todo_tool_name = TODO_WRITE_TOOL_NAME
178
194
  todo_available = todo_tool_name in tool_names
179
- task_available = "Task" in tool_names
180
- shell_tool_name = next((tool.name for tool in tools if tool.name.lower() == "bash"), "Bash")
195
+ task_available = TASK_TOOL_NAME in tool_names
196
+ ask_tool_name = ASK_USER_QUESTION_TOOL_NAME
197
+ ask_available = ask_tool_name in tool_names
198
+ view_tool_name = VIEW_TOOL_NAME
199
+ file_edit_tool_name = FILE_EDIT_TOOL_NAME
200
+ file_write_tool_name = FILE_WRITE_TOOL_NAME
201
+ shell_tool_name = next(
202
+ (tool.name for tool in tools if tool.name.lower() == BASH_TOOL_NAME.lower()),
203
+ BASH_TOOL_NAME,
204
+ )
181
205
 
182
206
  main_prompt = dedent(
183
207
  f"""\
@@ -190,61 +214,25 @@ def build_system_prompt(
190
214
  - /help: Get help with using {APP_NAME}
191
215
  - To give feedback, users should report the issue at {FEEDBACK_URL}
192
216
 
193
- # Tone and style
194
- You should be concise, direct, and to the point.
195
- You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless user asks for detail.
196
- IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand, avoiding tangential information unless absolutely critical for completing the request. If you can answer in 1-3 sentences or a short paragraph, please do.
197
- IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
198
- Do not add additional code explanation summary unless requested by the user. After working on a file, just stop, rather than providing an explanation of what you did.
199
- Answer the user's question directly, without elaboration, explanation, or details. One word answers are best. Avoid introductions, conclusions, and explanations. You MUST avoid text before/after your response, such as "The answer is <answer>.", "Here is the content of the file..." or "Based on the information provided, the answer is..." or "Here is what I will do next...". Here are some examples to demonstrate appropriate verbosity:
200
- <example>
201
- user: 2 + 2
202
- assistant: 4
203
- </example>
204
-
205
- <example>
206
- user: what is 2+2?
207
- assistant: 4
208
- </example>
217
+ # Looking up your own documentation
218
+ When the user asks what {APP_NAME} can do, how to use it (hooks, slash commands, MCP, SDKs), or requests SDK code samples, use the {TASK_TOOL_NAME} tool with a documentation-focused subagent (for example, subagent_type="docs") if available to consult official docs before answering.
209
219
 
210
- <example>
211
- user: is 11 a prime number?
212
- assistant: Yes
213
- </example>
214
-
215
- <example>
216
- user: what command should I run to list files in the current directory?
217
- assistant: ls
218
- </example>
219
-
220
- <example>
221
- user: what command should I run to watch files in the current directory?
222
- assistant: [use the ls tool to list the files in the current directory, then read docs/commands in the relevant file to find out how to watch files]
223
- npm run dev
224
- </example>
225
-
226
- <example>
227
- user: How many golf balls fit inside a jetta?
228
- assistant: 150000
229
- </example>
230
-
231
- <example>
232
- user: what files are in the directory src/?
233
- assistant: [runs ls and sees foo.c, bar.c, baz.c]
234
- user: which file contains the implementation of foo?
235
- assistant: src/foo.c
236
- </example>
237
-
238
- <example>
239
- user: write tests for new feature
240
- assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit file tool to write new tests]
241
- </example>
220
+ # Tone and style
221
+ - Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
222
+ - Your output will be displayed on a command line interface. Your responses should be short and concise. You can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
223
+ - Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like {BASH_TOOL_NAME} or code comments as means to communicate with the user during the session.
224
+ - NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files.
225
+
226
+ # Professional objectivity
227
+ Prioritize technical accuracy and truthfulness over validating the user's beliefs. Focus on facts and problem-solving, providing direct, objective technical info without any unnecessary superlatives, praise, or emotional validation. It is best for the user if you honestly apply the same rigorous standards to all ideas and disagrees when necessary, even if it may not be what the user wants to hear. Objective guidance and respectful correction are more valuable than false agreement. Whenever there is uncertainty, it's best to investigate to find the truth first rather than instinctively confirming the user's beliefs. Avoid using over-the-top validation or excessive praise when responding to users such as "You're absolutely right" or similar phrases.
228
+
229
+ # Planning without timelines
230
+ When planning tasks, provide concrete implementation steps without time estimates. Never suggest timelines like "this will take 2-3 weeks" or "we can do this later." Focus on what needs to be done, not when. Break work into actionable steps and let users decide scheduling.
231
+
232
+ # Explain Your Code: Bash Command Transparency
242
233
  When you run a non-trivial bash command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing (this is especially important when you are running a command that will make changes to the user's system).
243
234
  Remember that your output will be displayed on a command line interface. Your responses can use Github-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
244
- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like {shell_tool_name} or code comments as means to communicate with the user during the session.
245
235
  If you cannot or will not help the user with something, please do not say why or what it could lead to, since this comes across as preachy and annoying. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
246
- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
247
- IMPORTANT: Keep your responses short, since they will be displayed on a command line interface.
248
236
 
249
237
  # Proactiveness
250
238
  You are allowed to be proactive, but only when the user asks you to do something. You should strive to strike a balance between:
@@ -260,7 +248,7 @@ def build_system_prompt(
260
248
  - Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
261
249
 
262
250
  # Code style
263
- - IMPORTANT: DO NOT ADD ***ANY*** COMMENTS unless asked"""
251
+ - Only add comments when the logic is not self-evident and within code you changed. Do not add docstrings, comments, or type annotations to code you did not modify."""
264
252
  ).strip()
265
253
 
266
254
  if mcp_instructions:
@@ -318,6 +306,15 @@ def build_system_prompt(
318
306
  </example>"""
319
307
  ).strip()
320
308
 
309
+ ask_questions_section = ""
310
+ if ask_available:
311
+ ask_questions_section = dedent(
312
+ f"""\
313
+ # Asking questions as you work
314
+
315
+ You have access to the {ask_tool_name} tool to ask the user questions when you need clarification, want to validate assumptions, or need to make a decision you're unsure about. When presenting options or plans, do not include time estimates—focus on what each option involves."""
316
+ ).strip()
317
+
321
318
  hooks_section = dedent(
322
319
  """\
323
320
  Users may configure 'hooks', shell commands that execute in response to events like tool calls, in settings. Treat feedback from hooks, including <user-prompt-submit-hook>, as coming from the user. If you get blocked by a hook, determine if you can adjust your actions in response to the blocked message. If not, ask the user to check their hooks configuration."""
@@ -329,15 +326,26 @@ def build_system_prompt(
329
326
  ]
330
327
  if todo_available:
331
328
  doing_tasks_lines.append(f"- Use the {todo_tool_name} tool to plan the task if required")
329
+ if ask_available:
330
+ doing_tasks_lines.append(
331
+ f"- Use the {ask_tool_name} tool to ask questions, clarify, and gather information as needed."
332
+ )
332
333
  doing_tasks_lines.extend(
333
334
  [
335
+ "- NEVER propose changes to code you haven't read. If a user asks about or wants you to modify a file, read it first.",
334
336
  "- Use the available search tools to understand the codebase and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially.",
335
- "- Implement the solution using all tools available to you",
337
+ "- When exploring the codebase beyond a needle query, prefer using the Task tool with an exploration subagent if available instead of running raw search commands directly.",
338
+ "- Implement the solution using all tools available to you.",
339
+ "- Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities. If you notice that you wrote insecure code, immediately fix it.",
340
+ "- Avoid over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused.",
341
+ " - Don't add features, refactor code, or make improvements beyond what was asked. Don't add docstrings, comments, or type annotations to code you didn't change. Only add comments where the logic isn't self-evident.",
342
+ " - Don't add error handling, fallbacks, or validation for scenarios that can't happen. Validate only at system boundaries (user input, external APIs).",
343
+ " - Don't create helpers, utilities, or abstractions for one-time operations. Avoid feature flags or backwards-compatibility shims when a direct change is sufficient. If something is unused, delete it completely.",
336
344
  "- Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.",
337
345
  f"- VERY IMPORTANT: When you have completed a task, you MUST run the lint and typecheck commands (eg. npm run lint, npm run typecheck, ruff, etc.) with {shell_tool_name} if they were provided to you to ensure your code is correct. If you are unable to find the correct command, ask the user for the command to run and if they supply it, proactively suggest writing it to AGENTS.md so that you will know to run it next time.",
338
346
  "NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.",
339
- "",
340
347
  "- Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result.",
348
+ "- The conversation has unlimited context through automatic summarization. Complete tasks fully; do not stop mid-task or claim context limits.",
341
349
  ]
342
350
  )
343
351
  doing_tasks_section = "\n".join(doing_tasks_lines)
@@ -345,7 +353,8 @@ def build_system_prompt(
345
353
  tool_usage_lines = [
346
354
  "# Tool usage policy",
347
355
  '- You have the capability to call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance. When making multiple bash tool calls, you MUST send a single message with multiple tools calls to run the calls in parallel. For example, if you need to run "git status" and "git diff", send a single message with two tool calls to run the calls in parallel.',
348
- "",
356
+ "- If the user asks to run tools in parallel and there are no dependencies, include multiple tool calls in a single message; sequence dependent calls instead of guessing values.",
357
+ f"- Use specialized tools instead of bash when possible: use {view_tool_name} for reading files, {file_edit_tool_name} for editing, and {file_write_tool_name} for creating files. Do not use bash echo or other command-line tools to communicate with the user; reply in text.",
349
358
  "You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.",
350
359
  ]
351
360
  if task_available:
@@ -353,7 +362,7 @@ def build_system_prompt(
353
362
  1,
354
363
  "- Use the Task tool with configured subagents when the task matches an agent's description. Always set subagent_type.",
355
364
  )
356
- if "ToolSearch" in tool_names:
365
+ if TOOL_SEARCH_TOOL_NAME in tool_names:
357
366
  tool_usage_lines.insert(
358
367
  1,
359
368
  "- Use the ToolSearch tool to discover and activate deferred or MCP tools. Keep searches focused and load only 3-5 relevant tools.",
@@ -381,8 +390,11 @@ def build_system_prompt(
381
390
 
382
391
  Provide detailed prompts so the agent can work autonomously and return a concise report."""
383
392
  ).strip()
384
- except Exception as exc:
385
- logger.exception("Failed to load agent definitions", extra={"error": str(exc)})
393
+ except (OSError, ValueError, RuntimeError) as exc:
394
+ logger.warning(
395
+ "Failed to load agent definitions: %s: %s",
396
+ type(exc).__name__, exc,
397
+ )
386
398
  agent_section = (
387
399
  "# Subagents\nTask tool available, but agent definitions could not be loaded."
388
400
  )
@@ -402,14 +414,14 @@ def build_system_prompt(
402
414
  sections: List[str] = [
403
415
  main_prompt,
404
416
  task_management_section,
417
+ ask_questions_section,
405
418
  hooks_section,
406
419
  doing_tasks_section,
407
420
  tool_usage_section,
408
421
  agent_section,
409
422
  build_environment_prompt(),
410
- DEFENSIVE_SECURITY_GUIDELINE,
411
423
  always_use_todo,
412
- build_commit_workflow_prompt(shell_tool_name, todo_tool_name, "Task"),
424
+ build_commit_workflow_prompt(shell_tool_name, todo_tool_name, TASK_TOOL_NAME),
413
425
  code_references,
414
426
  ]
415
427