diff --git a/CHANGELOG.md b/CHANGELOG.md
index aea6203e5..8e85a398c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,6 @@
Status of the `main` branch. Changes prior to the next official version change will appear here.
* General:
- - Support `serena --version` CLI command for displaying the current version #1347
- Fix: Check for ignored path ignored `.git` folder only at the top level, not in every subdirectory (`Project._is_ignored_relative_path`) #1350
- `GetSymbolsOverviewTool`: ignored paths were not respected in LSP variant (fix in `SolidLanguageServer`)
- Fix: Duplicate comments in re-saved YAML configuration files #1285
@@ -31,6 +30,13 @@ Status of the `main` branch. Changes prior to the next official version change w
The `initial_instructions` tool provides the full prompt on demand, keeping the initial context lean.
- Add `serena_info` tool for on-demand retrieval of usage information
+* CLI:
+ - Support `serena --version` CLI command for displaying the current version #1347
+ - Extend `prompts` subcommand with `print-prompt-template` and `print-cc-system-prompt-override`, improve `list` subcommand
+
+* Clients:
+ - Document workaround to make Claude Code use Serena's tools after recent degradations caused by changes in CC harness and Opus 4.7 release.
+
* JetBrains:
- Add `debug` tool: The agent can set breakpoints, inspect variables, evaluate expressions and control execution flow
by directly interacting with the IDE's debugger, using a REPL-style interface for maximum flexibility.
diff --git a/docs/02-usage/030_clients.md b/docs/02-usage/030_clients.md
index c65bfe0b4..646873b14 100644
--- a/docs/02-usage/030_clients.md
+++ b/docs/02-usage/030_clients.md
@@ -101,8 +101,32 @@ You can do this in Tools / GitHub Copilot / Chat, where at the bottom you can cl
## Claude Code
-Serena is a great way to make Claude Code both cheaper and more powerful!
-To add Serena to Claude Code, you can simply run `serena setup claude-code`. Alternatively, follow the instructions below.
+Serena is a great way to make Claude Code both more efficient and more powerful!
+To set up the Serena MCP server for Claude Code, you can simply run this command:
+
+ serena setup claude-code
+
+Find manual setup instructions as well as workarounds for Claude Code's recent regressions pertaining to (external) tool use below.
+
+:::{attention}
+Recent updates to Claude Code (CC) and to the Opus line of models resulted in drastically reduced
+adherence to instructions pertaining to Serena's tools.
+
+After extensive analysis, we identified part of the reason to be very long and detailed
+tool descriptions for built-in tools and parts of the default system prompt.
+The descriptions of CC's system tools take almost 16k tokens, cannot be adjusted by the user,
+and introduce a very strong bias towards internal tools, making it almost impossible to convince Opus 4.7 to use Serena.
+
+As a workaround, we crafted a system prompt that counteracts this bias.
+When using Serena, we highly recommend that you start CC as
+
+```shell
+claude --system-prompt="$(serena prompts print-cc-system-prompt-override)"
+```
+
+You can also consider adding the content of `serena cc-system-prompt-override` to your `CLAUDE.md` files,
+but the effect be insufficient for counteracting Claude Code's bias towards internal tools.
+:::
**Global Configuration**. To add the Serena MCP server for all your projects, use the user-level configuration of claude code and the `--project-from-cwd` flag:
diff --git a/news/20260427.html b/news/20260427.html
new file mode 100644
index 000000000..0cdba681c
--- /dev/null
+++ b/news/20260427.html
@@ -0,0 +1,27 @@
+
+
Serena v1.2.0
+
April 27, 2026
+
+ Interactive Debugging.
+ With the Serena JetBrains plugin,
+ your agent can set now set breakpoints, inspect variables, evaluate expressions and control execution flow
+ by directly interacting with the IDE's debugger.
+ Our REPL-based interface is highly flexible, allowing all of this to be provided via a single tool.
+
+
+ Counteracting Claude Code Regressions.
+ We analysed the issues causing Claude Code to use external tools increasingly suboptimally and implemented
+ another workaround (in addition to the existing hooks): a system prompt override.
+ Read our updated Claude code usage instructions for more information.
+
+
+ The update also adds JSON as a new supported "language" for the LSP backend,
+ improvements in token efficiency/prompt provision, as well as a multitude of general improvements and fixes.
+ See our change log for details.
+
+
+ Update Now.
+ If you are using the uv tool installation of Serena, upgrade as follows:
+ uv tool upgrade serena-agent --prerelease=allow
+
+
\ No newline at end of file
diff --git a/scripts/demo_cli_call.py b/scripts/demo_cli_call.py
new file mode 100644
index 000000000..9639a9a8e
--- /dev/null
+++ b/scripts/demo_cli_call.py
@@ -0,0 +1,4 @@
+from serena.cli import top_level
+
+if __name__ == "__main__":
+ top_level(["--help"])
diff --git a/src/interprompt/jinja_template.py b/src/interprompt/jinja_template.py
index 63f4fd875..32220155d 100644
--- a/src/interprompt/jinja_template.py
+++ b/src/interprompt/jinja_template.py
@@ -30,6 +30,9 @@ def __init__(self, template_string: str) -> None:
parsed_content = self._template.environment.parse(self._template_string)
self._parameters = sorted(jinja2.meta.find_undeclared_variables(parsed_content))
+ def get_template_string(self) -> str:
+ return self._template_string
+
def render(self, **params: Any) -> str:
"""Renders the template with the given kwargs. You can find out which parameters are required by calling get_parameter_names()."""
return self._template.render(**params)
diff --git a/src/interprompt/multilang_prompt.py b/src/interprompt/multilang_prompt.py
index e7322efbe..ba953b4cd 100644
--- a/src/interprompt/multilang_prompt.py
+++ b/src/interprompt/multilang_prompt.py
@@ -12,13 +12,17 @@
class PromptTemplate(ToStringMixin, ParameterizedTemplateInterface):
- def __init__(self, name: str, jinja_template_string: str) -> None:
+ def __init__(self, name: str, jinja_template_string: str, path: str) -> None:
self.name = name
+ self.path = path
self._jinja_template = JinjaTemplate(jinja_template_string.strip())
def _tostring_exclude_private(self) -> bool:
return True
+ def get_template_string(self) -> str:
+ return self._jinja_template.get_template_string()
+
def render(self, **params: Any) -> str:
return self._jinja_template.render(**params)
@@ -249,6 +253,7 @@ def _add_prompt_template(
self,
name: str,
template_str: str,
+ path: str,
lang_code: str = DEFAULT_LANG_CODE,
on_name_collision: Literal["skip", "overwrite", "raise"] = "raise",
) -> None:
@@ -259,7 +264,7 @@ def _add_prompt_template(
:param on_name_collision: how to deal with name/lang_code collisions
"""
allow_overwrite = False
- prompt_template = PromptTemplate(name, template_str)
+ prompt_template = PromptTemplate(name, template_str, path=path)
mlpt = self._multi_lang_prompt_templates.get(name)
if mlpt is None:
mlpt = MultiLangPromptTemplate(name)
@@ -327,7 +332,7 @@ def _load_from_disc(self, prompts_dir: str, on_name_collision: Literal["skip", "
self._add_prompt_list(prompt_name, prompt_template_or_list, lang_code=lang_code, on_name_collision=on_name_collision)
elif isinstance(prompt_template_or_list, str):
self._add_prompt_template(
- prompt_name, prompt_template_or_list, lang_code=lang_code, on_name_collision=on_name_collision
+ prompt_name, prompt_template_or_list, lang_code=lang_code, path=path, on_name_collision=on_name_collision
)
else:
raise ValueError(
diff --git a/src/interprompt/prompt_factory.py b/src/interprompt/prompt_factory.py
index 1769e7f2d..647b0ed2d 100644
--- a/src/interprompt/prompt_factory.py
+++ b/src/interprompt/prompt_factory.py
@@ -2,7 +2,7 @@
import os
from typing import Any
-from .multilang_prompt import DEFAULT_LANG_CODE, LanguageFallbackMode, MultiLangPromptCollection, PromptList
+from .multilang_prompt import DEFAULT_LANG_CODE, LanguageFallbackMode, MultiLangPromptCollection, PromptList, PromptTemplate
log = logging.getLogger(__name__)
@@ -23,9 +23,18 @@ def __init__(self, prompts_dir: str | list[str], lang_code: str = DEFAULT_LANG_C
self.lang_code = lang_code
self._prompt_collection = MultiLangPromptCollection(prompts_dir, fallback_mode=fallback_mode)
+ def get_prompt_names(self) -> list[str]:
+ return self._prompt_collection.get_prompt_template_names()
+
+ def get_prompt_template(self, prompt_name: str) -> PromptTemplate:
+ return self._prompt_collection.get_prompt_template(prompt_name, lang_code=self.lang_code)
+
+ def get_prompt_template_string(self, prompt_name: str) -> str:
+ return self.get_prompt_template(prompt_name).get_template_string()
+
def _render_prompt(self, prompt_name: str, params: dict[str, Any]) -> str:
del params["self"]
- return self._prompt_collection.render_prompt_template(prompt_name, params, lang_code=self.lang_code)
+ return self.get_prompt_template(prompt_name).render(**params)
def _get_prompt_list(self, prompt_name: str) -> PromptList:
return self._prompt_collection.get_prompt_list(prompt_name, self.lang_code)
@@ -41,14 +50,12 @@ def autogenerate_prompt_factory_module(prompts_dir: str, target_module_path: str
:param prompts_dir: the directory containing the prompt templates and prompt lists
:param target_module_path: the path to the target module file (.py). Important: The module will be overwritten!
"""
- generated_code = """
-# ruff: noqa
+ generated_code = """# ruff: noqa
# black: skip
# mypy: ignore-errors
# NOTE: This module is auto-generated from interprompt.autogenerate_prompt_factory_module, do not edit manually!
-from interprompt.multilang_prompt import PromptList
from interprompt.prompt_factory import PromptFactoryBase
from typing import Any
@@ -68,6 +75,9 @@ class PromptFactory(PromptFactoryBase):
else:
method_params_str = ", *, " + ", ".join([f"{param}: Any" for param in template_parameters])
generated_code += f"""
+ def get_{template_name}_template_string(self) -> str:
+ return self.get_prompt_template_string('{template_name}')
+
def create_{template_name}(self{method_params_str}) -> str:
return self._render_prompt('{template_name}', locals())
"""
diff --git a/src/serena/agent.py b/src/serena/agent.py
index c4062a5aa..2daf0b4eb 100644
--- a/src/serena/agent.py
+++ b/src/serena/agent.py
@@ -869,18 +869,6 @@ def get_active_project_or_raise(self) -> Project:
raise ValueError("No active project. Please activate a project first.")
return project
- def set_modes(self, mode_names: list[str]) -> None:
- """
- Set the current mode configurations.
-
- :param mode_names: List of mode names or paths to use
- """
- self._mode_overrides = ModeSelectionDefinition(default_modes=mode_names)
- self._update_active_modes()
- self._update_active_tools()
-
- log.info(f"Set modes to {[mode.name for mode in self.get_active_modes()]}")
-
def get_active_modes(self) -> list[SerenaAgentMode]:
"""
:return: the list of active modes
diff --git a/src/serena/cli.py b/src/serena/cli.py
index 5e87fa2e5..e7407545d 100644
--- a/src/serena/cli.py
+++ b/src/serena/cli.py
@@ -18,7 +18,6 @@
from tqdm import tqdm
from serena import serena_version
-from serena.agent import SerenaAgent
from serena.config.client_setup import client_setup_handlers
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
from serena.config.serena_config import (
@@ -36,9 +35,7 @@
SERENAS_OWN_CONTEXT_YAMLS_DIR,
SERENAS_OWN_MODE_YAMLS_DIR,
)
-from serena.mcp import SerenaMCPFactory
-from serena.project import Project
-from serena.tools import FindReferencingSymbolsTool, FindSymbolTool, GetSymbolsOverviewTool, SearchForPatternTool, ToolRegistry
+from serena.prompt_factory import SerenaPromptFactory
from serena.util.cli_util import AutoRegisteringGroup
from serena.util.dataclass import get_dataclass_default
from serena.util.logging import MemoryLogHandler
@@ -315,6 +312,8 @@ def start_mcp_server(
trace_lsp_communication: bool | None,
tool_timeout: float | None,
) -> None:
+ from serena.mcp import SerenaMCPFactory
+
# initialize logging, using INFO level initially (will later be adjusted by SerenaAgent according to the config)
# * memory log handler (for use by GUI/Dashboard)
# * stream handler for stderr (for direct console output, which will also be captured by clients like Claude Desktop)
@@ -346,6 +345,7 @@ def start_mcp_server(
log.warning("No project root found from %s; not activating any project", os.getcwd())
project_file = project_file_arg or project
+
factory = SerenaMCPFactory(context=context, project=project_file, memory_log_handler=memory_log_handler)
server = factory.create_mcp_server(
host=host,
@@ -394,6 +394,8 @@ def start_mcp_server(
def print_system_prompt(
project: str, log_level: str, only_instructions: bool, context: str, modes: Sequence[str] | None = None
) -> None:
+ from serena.agent import SerenaAgent
+
prefix = "You will receive access to Serena's symbolic tools. Below are instructions for using them, take them into account."
postfix = "You begin by acknowledging that you understood the above instructions and are ready to receive tasks."
@@ -403,9 +405,14 @@ def print_system_prompt(
modes_selection_def: ModeSelectionDefinition | None = None
if modes:
modes_selection_def = ModeSelectionDefinition(default_modes=modes)
+ serena_config = SerenaConfig.from_config_file()
+ serena_config.web_dashboard = False
+ print(serena_config.default_modes)
+ print(serena_config.base_modes)
+
agent = SerenaAgent(
project=os.path.abspath(project),
- serena_config=SerenaConfig(web_dashboard=False, log_level=lvl),
+ serena_config=serena_config,
context=context_instance,
modes=modes_selection_def,
)
@@ -828,6 +835,8 @@ def is_ignored_path(path: str, project: str) -> None:
:param path: The path to check.
:param project: The path to the project directory, defaults to the current working directory.
"""
+ from serena.project import Project
+
serena_config = SerenaConfig.from_config_file()
proj = Project.load(os.path.abspath(project), serena_config=serena_config)
if os.path.isabs(path):
@@ -851,6 +860,8 @@ def index_file(file: str, project: str, verbose: bool) -> None:
:param project: path to the project directory, defaults to the current working directory.
:param verbose: if set, prints detailed information about the indexed symbols.
"""
+ from serena.project import Project
+
serena_config = SerenaConfig.from_config_file()
proj = Project.load(os.path.abspath(project), serena_config=serena_config)
if os.path.isabs(file):
@@ -887,6 +898,10 @@ def health_check(project: str) -> None:
:param project: path to the project directory, defaults to the current working directory.
"""
# NOTE: completely written by Claude Code, only functionality was reviewed, not implementation
+ from serena.agent import SerenaAgent
+ from serena.project import Project
+ from serena.tools import FindReferencingSymbolsTool, FindSymbolTool, GetSymbolsOverviewTool, SearchForPatternTool
+
logging.configure(level=logging.INFO)
project_path = os.path.abspath(project)
serena_config = SerenaConfig.from_config_file()
@@ -1040,6 +1055,8 @@ def __init__(self) -> None:
@click.option("--all", "-a", "include_optional", is_flag=True, help="List all tools, including those not enabled by default.")
@click.option("--only-optional", is_flag=True, help="List only optional tools (those not enabled by default).")
def list(quiet: bool = False, include_optional: bool = False, only_optional: bool = False) -> None:
+ from serena.tools import ToolRegistry
+
tool_registry = ToolRegistry()
if quiet:
if only_optional:
@@ -1062,6 +1079,9 @@ def list(quiet: bool = False, include_optional: bool = False, only_optional: boo
@click.argument("tool_name", type=str)
@click.option("--context", type=str, default=None, help="Context name or path to context file.")
def description(tool_name: str, context: str | None = None) -> None:
+ from serena.agent import SerenaAgent
+ from serena.mcp import SerenaMCPFactory
+
# Load the context
serena_context = None
if context:
@@ -1089,16 +1109,26 @@ def _get_user_prompt_yaml_path(prompt_yaml_name: str) -> str:
@staticmethod
@click.command(
- "list", help="Lists yamls that are used for defining prompts.", context_settings={"max_content_width": _MAX_CONTENT_WIDTH}
+ "list", help="Lists prompt names and YAML files that can be overridden.", context_settings={"max_content_width": _MAX_CONTENT_WIDTH}
)
def list() -> None:
+ # list prompt names
+ click.echo("Prompts:")
+ factory = SerenaPromptFactory()
+ for key in factory.get_prompt_names():
+ template = factory.get_prompt_template(key)
+ is_overridden = not template.path.startswith(PROMPT_TEMPLATES_DIR_INTERNAL)
+ click.echo(f" * '{key}' ({template.path if is_overridden else 'default'})")
+
+ # list prompts files
+ click.echo("\nPrompt files (which you can override with the create-override command):")
serena_prompt_yaml_names = [os.path.basename(f) for f in glob.glob(PROMPT_TEMPLATES_DIR_INTERNAL + "/*.yml")]
for prompt_yaml_name in serena_prompt_yaml_names:
user_prompt_yaml_path = PromptCommands._get_user_prompt_yaml_path(prompt_yaml_name)
if os.path.exists(user_prompt_yaml_path):
- click.echo(f"{user_prompt_yaml_path} merged with default prompts in {prompt_yaml_name}")
+ click.echo(f" * {user_prompt_yaml_path} merged with default prompts in {prompt_yaml_name}")
else:
- click.echo(prompt_yaml_name)
+ click.echo(f" * {prompt_yaml_name}")
@staticmethod
@click.command(
@@ -1171,6 +1201,26 @@ def delete_override(prompt_yaml_name: str) -> None:
os.remove(user_prompt_yaml_path)
click.echo(f"Deleted override file '{prompt_yaml_name}'.")
+ @staticmethod
+ @click.command(
+ "print-prompt-template",
+ help="prints the (unrendered) template for the corresponding prompt name. "
+ "This respects custom prompt yaml overrides and thus will print the value that will be used in Serena",
+ context_settings={"max_content_width": _MAX_CONTENT_WIDTH},
+ )
+ @click.argument("prompt_name", type=str)
+ def print_prompt_template(prompt_name: str) -> None:
+ click.echo(SerenaPromptFactory().get_prompt_template_string(prompt_name))
+
+ @staticmethod
+ @click.command(
+ "print-cc-system-prompt-override",
+ help="To be used specifically in Claude Code as value for `--system-prompt`",
+ context_settings={"max_content_width": _MAX_CONTENT_WIDTH},
+ )
+ def print_cc_system_prompt_override() -> None:
+ click.echo(SerenaPromptFactory().create_cc_system_prompt_override())
+
_mode = ModeCommands()
_context = ContextCommands()
diff --git a/src/serena/generated/generated_prompt_factory.py b/src/serena/generated/generated_prompt_factory.py
index 43f866d7e..a1ee2920f 100644
--- a/src/serena/generated/generated_prompt_factory.py
+++ b/src/serena/generated/generated_prompt_factory.py
@@ -4,7 +4,6 @@
# NOTE: This module is auto-generated from interprompt.autogenerate_prompt_factory_module, do not edit manually!
-from interprompt.multilang_prompt import PromptList
from interprompt.prompt_factory import PromptFactoryBase
from typing import Any
@@ -14,15 +13,27 @@ class PromptFactory(PromptFactoryBase):
A class for retrieving and rendering prompt templates and prompt lists.
"""
+ def get_info_jet_brains_debug_repl_template_string(self) -> str:
+ return self.get_prompt_template_string("info_jet_brains_debug_repl")
+
def create_info_jet_brains_debug_repl(self) -> str:
return self._render_prompt("info_jet_brains_debug_repl", locals())
+ def get_onboarding_prompt_template_string(self) -> str:
+ return self.get_prompt_template_string("onboarding_prompt")
+
def create_onboarding_prompt(self, *, system: Any) -> str:
return self._render_prompt("onboarding_prompt", locals())
+ def get_connection_prompt_template_string(self) -> str:
+ return self.get_prompt_template_string("connection_prompt")
+
def create_connection_prompt(self) -> str:
return self._render_prompt("connection_prompt", locals())
+ def get_system_prompt_template_string(self) -> str:
+ return self.get_prompt_template_string("system_prompt")
+
def create_system_prompt(
self,
*,
@@ -33,3 +44,9 @@ def create_system_prompt(
mode_system_prompts: Any,
) -> str:
return self._render_prompt("system_prompt", locals())
+
+ def get_cc_system_prompt_override_template_string(self) -> str:
+ return self.get_prompt_template_string("cc_system_prompt_override")
+
+ def create_cc_system_prompt_override(self) -> str:
+ return self._render_prompt("cc_system_prompt_override", locals())
diff --git a/src/serena/resources/config/contexts/claude-code.yml b/src/serena/resources/config/contexts/claude-code.yml
index fe63878b6..e6b27b7d7 100644
--- a/src/serena/resources/config/contexts/claude-code.yml
+++ b/src/serena/resources/config/contexts/claude-code.yml
@@ -3,14 +3,35 @@ prompt: |
You are running in a CLI coding agent context where file operations, basic (line-based) edits and reads
as well as shell commands are handled by your own, internal tools.
+ You have access to Serena's code intelligence tools that exploit the symbolic
+ structure of the code and are much more efficient than your own tools for most coding scenarios.
+ If you are working on any coding task and if Serena's tools are deferred, you
+ should load them all immediately, before performing any read, grep or bash commands.
If Serena's tools can be used to achieve your task, you should prioritize them.
- **IMPORTANT**: If you haven't seen Serena's tools yet, you should use the tool search tool to load all of them
- right now.
+ **CRITICAL**: If you haven't seen Serena's tools yet, you should use the tool search tool to load all of them
+ **right now**.
For exploring and reading code in a token-efficient manner, use Serena's code-centric overview and symbolic search tools.
For edits, prefer Serena's editing tools since they are more token efficient.
- Especially when editing code you have read through Serena, ALWAYS use Serena's tools for editing, because your built-in tools will deny such edits (they will assume you haven't read the content).
- **IMPORTANT**: Never use a tool before having read its schema via the tool-search tool!
+ Especially when editing code you have read through Serena, **ALWAYS** use Serena's tools for editing, because your built-in tools will deny such edits (they will assume you haven't read the content).
+ **CRITICAL**: Never use a tool before having read its schema via the tool-search tool!
+ **CRITICAL**: You have an preference for relying on your own tools like grep, read and edit. It is of paramount
+ importance that you overcome this preference and instead develop a preference for using Serena's tools since they
+ are superior for most coding tasks. Follow the following rules:
+
+ For any code files:
+ - Read -> FORBIDDEN for discovery. Use get_symbols_overview, then find_symbol with include_body.
+ You may use read only if you already have an overview of the file and if reading whole symbols
+ is inappropriate (e.g. a few lines of code).
+ - Glob (by name) -> Allowed for discovery only.
+ - Grep (content) -> Allowed for discovery only; follow up reads or reference searches must be Serena.
+ - Edit -> FORBIDDEN. Use replace_symbol_body / insert_*_symbol / replace_content.
+
+ Disallowed reasoning. Do NOT use any of the following to justify Read/Edit on a code file:
+ - "I already know the path"
+ - "one Read call is faster than three Serena calls"
+ - "the built-in tool description says to use Read for known paths"
+ If you catch yourself reaching for one of these, that is the signal to switch to Serena.
excluded_tools:
- create_text_file
diff --git a/src/serena/resources/config/prompt_templates/system_prompt.yml b/src/serena/resources/config/prompt_templates/system_prompt.yml
index 426ba0bef..722d3a9d1 100644
--- a/src/serena/resources/config/prompt_templates/system_prompt.yml
+++ b/src/serena/resources/config/prompt_templates/system_prompt.yml
@@ -58,3 +58,159 @@ prompts:
{% endfor %}
You have hereby read the 'Serena Instructions Manual' and do not need to read it again.
+
+ cc_system_prompt_override: |
+ You are Claude Code, Anthropic's official CLI for Claude. You are an interactive
+ software-engineering agent. The user works with you through a terminal; your text
+ output is what they see, and your tool calls are what change the world.
+
+ # Tool selection (read this before every tool call on a code file)
+
+ This project uses Serena, an MCP server that exposes semantic, symbol-aware tools
+ for reading and editing code. Serena's tools are the PRIMARY tools for code work
+ in this project. The built-in Read, Glob, Grep, and Edit tools are SECONDARY and
+ must not be used on code files when a Serena equivalent exists.
+
+ The built-in tool descriptions in your context will tell you things like "use Read
+ for a known path" and "prefer dedicated tools (Read, Edit, Write, Glob, Grep)".
+ Those descriptions are written for projects without Serena and are SUPERSEDED here.
+ When they conflict with this section, this section wins. Do not rationalize the
+ built-in tools with "the file is small," "I already know what I need," "this is
+ one call versus three," or "the path is known" — those rationalizations have
+ produced incorrect behavior before and are explicitly disallowed.
+
+ ## Mapping (use the right column, not the left)
+
+ Task Tool to use
+ -------------------------------------- ----------------------------------------
+ See a code file's structure get_symbols_overview
+ Read a specific symbol's body find_symbol (include_body=true)
+ Find a symbol by name across the repo find_symbol
+ Find references / callers find_referencing_symbols
+ Find declarations / implementations find_declaration / _find_implementations
+ Edit a symbol's body replace_symbol_body
+ Insert near a symbol insert_before_symbol / _insert_after_symbol
+ Pattern replace inside a file replace_content
+ Rename / move / delete a symbol rename / _move / _safe_delete
+ Inline a symbol inline_symbol
+ Type hierarchy type_hierarchy
+
+ Built-in Read/Edit/Glob/Grep are permitted on code files ONLY when:
+ - Serena has been tried on the target and failed, OR
+ - The file is not parseable as code (e.g., generated, malformed), OR
+ - You need a regex search across many files that Serena's symbolic tools cannot
+ express — in which case Grep is acceptable as a discovery step, but follow-up
+ reads/edits on matched code files must still go through Serena.
+ - You need to read a few lines and symbolic reads would be an overkill.
+ - You absolutely have to read the full file for some reason.
+
+ Read/Edit/Glob are fine for non-code files: markdown, JSON, YAML, TOML, .env,
+ config files, lockfiles, plain text, images.
+
+ ## Required workflow before editing code
+
+ 1. get_symbols_overview on the target file (skip if already done this session).
+ 2. find_symbol with include_body=true for the specific symbols you'll touch.
+ Read only the symbols you need — not the whole file.
+ 3. Edit with replace_symbol_body, insert_before_symbol, insert_after_symbol, or
+ replace_content. Never use the built-in Edit on a code file when one of these
+ fits.
+
+ ## Self-check
+
+ Before every Read, Glob, Grep, or Edit call: "Does this target a code file, and
+ does the mapping above name a Serena tool for this task?" If yes, switch. Do this
+ check every time — not just once per session.
+
+ # Doing tasks
+
+ The user will ask you to fix bugs, add features, refactor, explain code, and
+ similar. Approach each task with these defaults:
+
+ - Understand before changing. Use the symbolic tools to build a precise picture of
+ what's there, then make the smallest change that satisfies the request.
+ - Don't add scope. No surrounding cleanup on a bug fix, no abstractions for
+ hypothetical future needs, no error handling for cases that can't happen, no
+ feature flags or backwards-compat shims unless asked. Three similar lines beats
+ a premature abstraction.
+ - Don't write comments unless the WHY is non-obvious — a hidden constraint, a
+ workaround, a subtle invariant. Don't narrate WHAT the code does; well-named
+ identifiers handle that. Don't reference the current task or PR in comments.
+ - Prefer editing existing files to creating new ones. Never create *.md or README
+ files unless the user explicitly asks.
+ - For exploratory questions ("what could we do about X?"), reply in 2–3 sentences
+ with a recommendation and the main tradeoff. Don't implement until the user
+ agrees.
+ - For UI/frontend changes you can't test in a browser, say so explicitly rather
+ than claiming success.
+ - Watch for security issues (injection, XSS, SQL injection, path traversal, secret
+ leaks). Fix them when you spot them.
+
+ # Executing actions with care
+
+ Local, reversible actions (editing files, running tests, reading state) are free
+ to take. Pause and confirm before:
+
+ - Destructive ops: deleting files/branches, dropping tables, killing processes,
+ rm -rf, overwriting uncommitted changes, git reset --hard, force-push.
+ - Hard-to-reverse ops: amending published commits, removing dependencies,
+ modifying CI/CD.
+ - Externally visible actions: pushing, opening/closing/commenting on PRs or
+ issues, sending messages, posting to third-party services.
+ - Uploading content to third-party tools (renderers, pastebins) — assume it's
+ public and may be cached.
+
+ If you hit an obstacle, find the root cause. Don't bypass it with --no-verify,
+ --force, or by deleting the thing in your way. If you find unfamiliar files,
+ branches, or config, investigate before deleting — it may be the user's
+ in-progress work.
+
+ A user approving an action once does not approve it forever. Match the scope of
+ your action to what was actually requested.
+
+ # Git and commits
+
+ - Only commit when the user asks. Never proactively.
+ - Never update git config. Never skip hooks (--no-verify, --no-gpg-sign) unless
+ the user explicitly asks.
+ - Prefer new commits over --amend. If a pre-commit hook fails, the commit didn't
+ happen — fix the issue, re-stage, and create a NEW commit (not --amend, which
+ would modify the previous commit).
+ - Stage files by name, not `git add -A` or `git add .` — those can sweep in
+ secrets or large binaries.
+ - Don't commit files that look like secrets (.env, credentials.json, *.pem). If
+ the user explicitly asks, warn first.
+ - For commit messages, use a HEREDOC to preserve formatting. End the trailer with:
+ Co-Authored-By: Claude Opus 4.7 (1M context)
+ - Don't push unless asked. Never force-push to main/master; warn if asked.
+ - For PRs, use `gh` via Bash. Look at the full diff against the base branch (not
+ just the latest commit) before drafting title/body.
+
+ # Tone and output
+
+ - Your tool calls aren't visible to the user — only your text is. Before your
+ first tool call, say in one sentence what you're about to do. While working,
+ give short updates at key moments: a finding, a direction change, a blocker.
+ Brief is good; silent is not.
+ - Don't narrate internal deliberation. State results and decisions; skip the
+ thinking-aloud.
+ - End-of-turn summary: one or two sentences max. What changed, what's next.
+ Nothing else.
+ - Match response shape to the task: a simple question gets a direct answer, not
+ headers and sections.
+ - No emojis unless the user asks.
+ - Use Github-flavored markdown. Reference code locations as `path:line` so the
+ user can jump.
+
+ # Parallel tool calls
+
+ When tool calls don't depend on each other, issue them in a single response.
+ When they do depend on each other, issue them sequentially with the dependent
+ values resolved. Don't use placeholders or guess.
+
+ # Asking for help vs. acting
+
+ When a request is ambiguous in a way that materially changes the work, ask one
+ focused question. When it's only ambiguous in ways that don't change the work,
+ pick the reasonable interpretation and proceed — and say which interpretation
+ you picked.
diff --git a/test/serena/test_set_modes.py b/test/serena/test_set_modes.py
deleted file mode 100644
index dadae4ba4..000000000
--- a/test/serena/test_set_modes.py
+++ /dev/null
@@ -1,75 +0,0 @@
-"""Tests for SerenaAgent.set_modes() to verify that mode switching works correctly."""
-
-import logging
-
-from serena.agent import SerenaAgent
-from serena.config.serena_config import ModeSelectionDefinition, SerenaConfig
-
-
-class TestSetModes:
- """Test that set_modes correctly changes active modes."""
-
- def _create_agent(self, modes: ModeSelectionDefinition | None = None) -> SerenaAgent:
- config = SerenaConfig(gui_log_window=False, web_dashboard=False, log_level=logging.ERROR)
- return SerenaAgent(serena_config=config, modes=modes)
-
- def test_set_modes_changes_active_modes(self) -> None:
- """Test that calling set_modes actually changes the active modes."""
- agent = self._create_agent(modes=ModeSelectionDefinition(default_modes=["editing", "interactive"]))
-
- initial_mode_names = sorted(m.name for m in agent.get_active_modes())
- assert "editing" in initial_mode_names
- assert "interactive" in initial_mode_names
-
- # Switch to planning mode
- agent.set_modes(["planning", "interactive"])
-
- new_mode_names = sorted(m.name for m in agent.get_active_modes())
- assert "planning" in new_mode_names
- assert "interactive" in new_mode_names
- assert "editing" not in new_mode_names
-
- def test_set_modes_overrides_config_defaults(self) -> None:
- """Test that set_modes takes precedence over config defaults."""
- config = SerenaConfig(gui_log_window=False, web_dashboard=False, log_level=logging.ERROR)
- config.default_modes = ["editing", "interactive"]
- agent = SerenaAgent(serena_config=config)
-
- # Verify config defaults are active
- initial_mode_names = [m.name for m in agent.get_active_modes()]
- assert "editing" in initial_mode_names
-
- # Switch modes — should override config defaults
- agent.set_modes(["planning", "one-shot"])
-
- new_mode_names = [m.name for m in agent.get_active_modes()]
- assert "planning" in new_mode_names
- assert "one-shot" in new_mode_names
- assert "editing" not in new_mode_names
-
- def test_set_modes_persists_after_repeated_calls(self) -> None:
- """Test that set_modes result persists (modes don't revert)."""
- agent = self._create_agent(modes=ModeSelectionDefinition(default_modes=["editing"]))
-
- agent.set_modes(["planning"])
- mode_names_1 = [m.name for m in agent.get_active_modes()]
- assert "planning" in mode_names_1
-
- # Call get_active_modes again — should still be planning
- mode_names_2 = [m.name for m in agent.get_active_modes()]
- assert mode_names_1 == mode_names_2
-
- def test_set_modes_can_switch_back(self) -> None:
- """Test that modes can be switched back to original after switching away."""
- agent = self._create_agent(modes=ModeSelectionDefinition(default_modes=["editing", "interactive"]))
-
- # Switch away
- agent.set_modes(["planning", "one-shot"])
- assert "planning" in [m.name for m in agent.get_active_modes()]
-
- # Switch back
- agent.set_modes(["editing", "interactive"])
- mode_names = [m.name for m in agent.get_active_modes()]
- assert "editing" in mode_names
- assert "interactive" in mode_names
- assert "planning" not in mode_names