fix(webber-api): clear ruff, and two things it was pointing at

97 findings to zero. Most were mechanical — 52 unsorted import blocks, 10
unsorted __all__, assorted pyupgrade and simplify hints. Two were not, and both
were visible only because the lint made me look.

`webber version` did not exist. src/cli/commands/version.py defines
show_version(), main.py imported it, and the registration line was never
written — the CLI exposed chat and explore only. The import carried
`# noqa: F401`, which is what kept the omission quiet: someone marked the
symptom as intentional instead of asking why it was unused. show_version is not
redundant with the --version flag; it prints the resolved Ollama URL, model and
debug state, which is the form worth having when something is misconfigured.
Registered, and the suppression dropped because the import is now genuinely used.

test_spawn_explore_agent asserted nothing. It built a mock RunContext, patched
get_agent, and stopped at the comment "For now, verify the explore agent would
be called correctly". It had been counted as a passing test. An AST sweep of all
238 test functions found it was the only one, which is worth knowing — the
problem was contained, not systemic. It is now skipped with a reason, so it
reports as unfinished rather than as passing. Reducing it rather than deleting
its imports was the point: tidying the imports would have made a hollow test
look clean.

Two findings were false positives, and both are recorded rather than silently
worked around:

B023 flagged run_agent closing over full_prompt and ctx. Traced: agent_task is
awaited at line 326 before `continue` reaches the next iteration, so neither
name can be rebound while the closure is pending, and the exception path
cancels and awaits too. Not a bug. Bound as defaults anyway, because that stays
true if the await ever moves. I had called it a live bug before tracing it,
which is the mistake Rule 5 exists for.

RUF012 flagged `rules: list[ApprovalRule] = []` on ApprovalRuleSet. Its
suggested fix — annotate ClassVar — would remove the field from the model.
ApprovalRuleSet is a pydantic model and pydantic deep-copies defaults per
instance; verified by constructing two and confirming their lists are distinct
objects. Suppressed with that evidence in the comment. Ruff cannot see the
pydantic base because BaseSchema is a local subclass of BaseModel.

Also moved a stray `from src.shared.logging import ...` that had drifted below
a function definition, and merged a nested if in the ollama provider.

215 passed, 23 skipped, unchanged except for the new skip. `webber version`
exercised end to end.

mypy is NOT addressed here and the gate still fails on it — 55 errors in 14
files, 35 of them no-any-return from pydantic_ai's untyped returns. That was
hidden behind ruff, because the gate stops at the first failing stage.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-11 15:05:02 +02:00
co-authored by Claude
parent 9f5e331d11
commit eb3467d06a
50 changed files with 168 additions and 160 deletions
+8 -8
View File
@@ -4,19 +4,19 @@ Tool implementations for agent use.
All tools inherit from BaseTool and return ToolResult.
"""
from src.domains.tools.base import BaseTool, ToolResult
from src.domains.tools.file import ReadFileTool, GlobFilesTool, EditFileTool, WriteFileTool
from src.domains.tools.file import EditFileTool, GlobFilesTool, ReadFileTool, WriteFileTool
from src.domains.tools.search import GrepContentTool, WebSearchTool
from src.domains.tools.shell import BashReadOnlyTool, BashTool
__all__ = [
"BaseTool",
"ToolResult",
"ReadFileTool",
"GlobFilesTool",
"EditFileTool",
"WriteFileTool",
"GrepContentTool",
"WebSearchTool",
"BashReadOnlyTool",
"BashTool",
"EditFileTool",
"GlobFilesTool",
"GrepContentTool",
"ReadFileTool",
"ToolResult",
"WebSearchTool",
"WriteFileTool",
]
+1 -4
View File
@@ -32,10 +32,7 @@ class ToolResult:
if not self.success:
return f"ERROR: {self.error}"
if isinstance(self.data, str):
content = self.data
else:
content = str(self.data)
content = self.data if isinstance(self.data, str) else str(self.data)
if len(content) > max_length:
self.truncated = True
@@ -1,9 +1,9 @@
"""
File operation tools.
"""
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.edit import EditFileTool
from src.domains.tools.file.glob import GlobFilesTool
from src.domains.tools.file.read import ReadFileTool
from src.domains.tools.file.write import WriteFileTool
__all__ = ["ReadFileTool", "GlobFilesTool", "EditFileTool", "WriteFileTool"]
__all__ = ["EditFileTool", "GlobFilesTool", "ReadFileTool", "WriteFileTool"]
+6 -5
View File
@@ -2,11 +2,12 @@
File editing tool with find-and-replace functionality.
"""
import difflib
import aiofiles
from pathlib import Path
import aiofiles
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
@@ -147,15 +148,15 @@ Examples:
try:
# Read file content
async with aiofiles.open(path, 'r', encoding='utf-8', errors='replace') as f:
async with aiofiles.open(path, encoding='utf-8', errors='replace') as f:
content = await f.read()
# Check if old_string exists
count = content.count(old_string)
if count == 0:
return self._error(
f"old_string not found in file. "
f"Make sure to match exact whitespace and indentation."
"old_string not found in file. "
"Make sure to match exact whitespace and indentation."
)
# Check uniqueness if replace_all is False
+1 -1
View File
@@ -6,7 +6,7 @@ from pathlib import Path
from src.domains.tools.base import BaseTool, ToolResult
from src.domains.tools.gitignore import filter_gitignored
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
+4 -3
View File
@@ -1,11 +1,12 @@
"""
File reading tool with line number formatting and sandboxing.
"""
import aiofiles
from pathlib import Path
import aiofiles
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
@@ -87,7 +88,7 @@ IMPORTANT:
return self._error(f"Not a file: {file_path}")
try:
async with aiofiles.open(path, 'r', encoding='utf-8', errors='replace') as f:
async with aiofiles.open(path, encoding='utf-8', errors='replace') as f:
content = await f.read()
lines = content.splitlines()
+3 -2
View File
@@ -1,11 +1,12 @@
"""
File writing tool for creating and overwriting files.
"""
import aiofiles
from pathlib import Path
import aiofiles
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
+1 -4
View File
@@ -90,10 +90,7 @@ class GitignoreFilter:
# Make path relative to root for matching
try:
if path.is_absolute():
rel_path = path.resolve().relative_to(self.root_dir)
else:
rel_path = path
rel_path = path.resolve().relative_to(self.root_dir) if path.is_absolute() else path
except ValueError:
# Path is not under root_dir, don't filter
return False
+1 -1
View File
@@ -7,7 +7,7 @@ from typing import Literal
from src.domains.tools.base import BaseTool, ToolResult
from src.domains.tools.gitignore import filter_gitignored
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
+1 -1
View File
@@ -8,7 +8,7 @@ import httpx
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.config import get_settings
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
from src.shared.retry import retry_async
logger = get_logger(__name__)
+2 -2
View File
@@ -8,7 +8,7 @@ import shlex
from pathlib import Path
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
@@ -190,7 +190,7 @@ Examples:
exit_code=proc.returncode
)
except asyncio.TimeoutError:
except TimeoutError:
return self._error(f"Command timed out after {timeout} seconds")
except Exception as e:
logger.exception(f"Error executing command: {command}")
@@ -8,7 +8,7 @@ import shlex
from pathlib import Path
from src.domains.tools.base import BaseTool, ToolResult
from src.shared.logging import logged, get_logger
from src.shared.logging import get_logger, logged
logger = get_logger(__name__)
@@ -228,7 +228,7 @@ Examples:
exit_code=proc.returncode
)
except asyncio.TimeoutError:
except TimeoutError:
return self._error(f"Command timed out after {timeout} seconds")
except Exception as e:
logger.exception(f"Error executing command: {command}")
@@ -340,7 +340,7 @@ Examples:
# Handle git with flags before subcommand (e.g., git -C path status)
if git_subcommand.startswith("-"):
# Find the actual subcommand
for i, token in enumerate(tokens[2:], 2):
for _i, token in enumerate(tokens[2:], 2):
if not token.startswith("-"):
git_subcommand = token
break