Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a3da5b3
bd init: initialize beads issue tracking
chrisnestrud Sep 23, 2026
baaecd2
Sanitize MCP tool names for providers and map them back on dispatch
chrisnestrud Sep 25, 2026
c9d3d14
cli-65: retry config fix
Sep 26, 2026
75ce809
update
Sep 26, 2026
f27f157
merged
Sep 26, 2026
7e60027
cli-65: update linting
Sep 26, 2026
1cdded4
dwash96 Sep 26, 2026
e0522b6
cli-65: retry config fix (models.py + base_coder.py only, from PR #695)
Sep 26, 2026
4fa5b5f
cli-65: make parse_retry_config public (drop leading underscore)
Sep 26, 2026
bef3d19
Variant of #693: Fix at the source, use command_queue helper directly
dwash96 Sep 26, 2026
7ecceea
Merge branch 'v1.6.2-clean-695' into v1.6.2
dwash96 Sep 26, 2026
ea92c10
Merge pull request #694 from chrisnestrud/fix/mcp-tool-name-sanitization
dwash96 Sep 26, 2026
78294c1
Clean PR #694: drop .gitignore additions, map tool names via dicts
dwash96 Sep 26, 2026
59a3a0f
Detech command shells from terminals stdin stream so background comma…
dwash96 Sep 26, 2026
8100780
merged
Sep 26, 2026
52c0b36
feat: implement retry-on-forbidden feature
Sep 26, 2026
98f3e7e
#700: Add discrete ollama provider to handle local model routing para…
dwash96 Sep 26, 2026
8999518
style: fix formatting of retry_on_forbidden assignment
Sep 27, 2026
7a0b582
Update hook documentation for real name constraints
dwash96 Sep 27, 2026
0d83637
Don't reload finished agents or the memorizer on restore
dwash96 Sep 28, 2026
13cbaf0
Remove tips.md because it isn't relevant to cecli
dwash96 Sep 28, 2026
21845cb
fix: handle ValueError in find_common_root and remove unused functions
Sep 30, 2026
0be65bf
cli-68: fix cross drive file crash
Oct 1, 2026
028d756
cli-68: fixed cross drive file crashes
Oct 1, 2026
50daf98
cli-68: merged
Oct 1, 2026
206d022
fix: address linting errors in cecli/models.py and test_retry_config.py
Oct 1, 2026
8347974
feat: add opt-in retry-on-unauthorized for 401/403 auth errors
Oct 1, 2026
52ef307
cli-70: added retry-on-unauthorized
Oct 1, 2026
e776306
merged
Oct 1, 2026
3cb33fc
update
Oct 1, 2026
b070432
update
Oct 1, 2026
d3323b7
fix: Resolve linting errors in cecli/models.py
Oct 1, 2026
e2f817a
fix: honor retry-on-unauthorized and preserve retries settings
Oct 2, 2026
e7369d6
fixing the pickling error
Oct 2, 2026
fd47bee
Enhance error handling by using repr() for better debugging informati…
Oct 3, 2026
e7b7a9b
Allow kitty protocol default overrides for shift+enter detecton to wo…
dwash96 Oct 3, 2026
a3dec93
Fix /paste command in WSL and update ResourceManager to be less confu…
dwash96 Oct 3, 2026
f3bb231
Merge pull request #701 from szmania/cli-66-403-forbidden-retry
dwash96 Oct 3, 2026
9306b69
Re run compilation script to make sure deps files are well-formatted
dwash96 Oct 3, 2026
155d15e
Re-remove sessions.py
dwash96 Oct 3, 2026
c364fee
Merge remote-tracking branch 'szmania/cli-68-cross-drive-file-crash' …
dwash96 Oct 3, 2026
26ac936
Clean up cross-drive relpath handling into utils.safe_relpath
dwash96 Oct 3, 2026
8162847
Update _parse_retry_config
dwash96 Oct 3, 2026
9ed2391
Merge PR #705 (retry-on-unauthorized) into v1.6.2-clean-705
dwash96 Oct 3, 2026
3456e37
Merge pull request #707 from philippeback/fix/picklingerror
dwash96 Oct 3, 2026
d1b22f9
Add system one helper and hook intregration
dwash96 Oct 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion cecli/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"security_config",
"retries",
"custom",
"system_one",
"tui_config",
}
)
Expand Down Expand Up @@ -360,7 +361,7 @@ def get_parser(default_config_files, git_root):
metavar="RETRIES_JSON",
help=(
'Specify LLM retry configuration as a JSON/YAML string (e.g., \'{"retry_on_empty": '
"true}')"
'true, "retry-on-unauthorized": false}\')'
),
default=None,
)
Expand Down Expand Up @@ -427,6 +428,16 @@ def get_parser(default_config_files, git_root):
help="Specify hooks configuration as a JSON string",
default=None,
)
group.add_argument(
"--system-one",
metavar="SYSTEM_ONE_JSON",
help=(
"Specify the System One decision endpoint as a JSON/YAML string (e.g.,"
' \'{"api_base": "http://127.0.0.1:8000", "api_key_env": ["SYSTEM_ONE_API_KEY"],'
' "model_name": "von-latest"}\')'
),
default=None,
)
group.add_argument(
"--agent-model",
metavar="AGENT_MODEL",
Expand Down
1 change: 1 addition & 0 deletions cecli/args_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def _format_action(self, action):
parts.append("# retry-timeout: 60")
parts.append("# retry-backoff-factor: 2.0")
parts.append("# retry-on-unavailable: true")
parts.append("# retry-on-unauthorized: false")
parts.append("# retry-on-empty: false")
parts.append("")
return "\n".join(parts)
Expand Down
40 changes: 19 additions & 21 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
from cecli.linter import Linter
from cecli.llm import litellm
from cecli.mcp import LocalServer
from cecli.models import RETRY_TIMEOUT
from cecli.reasoning_tags import (
REASONING_TAG,
format_reasoning_content,
Expand All @@ -67,7 +66,7 @@
from cecli.run_cmd import run_cmd_async
from cecli.tools.utils.output import print_tool_response
from cecli.tools.utils.registry import ToolRegistry
from cecli.utils import copy_tool_call, format_tokens, is_image_file
from cecli.utils import copy_tool_call, format_tokens, is_image_file, safe_relpath

from ..dump import dump # noqa: F401
from ..prompts.utils.registry import PromptObject, PromptRegistry
Expand Down Expand Up @@ -2829,24 +2828,14 @@ async def format_in_executor():
except EmptyResponseError:
self.io.tool_warning(self.empty_llm_tool_warning())

retry_on_empty = False
retries_config = self.get_active_model().retries
if isinstance(retries_config, str):
try:
retries_config = json.loads(retries_config)
except json.JSONDecodeError:
self.io.tool_warning(
f"Could not parse retries config: {retries_config}"
)
retries_config = {}
if isinstance(retries_config, dict):
retry_on_empty = retries_config.get("retry_on_empty", False)
retry_config = models.parse_retry_config(self.get_active_model().retries)
retry_on_empty = retry_config["retry_on_empty"]

if not retry_on_empty:
break

retry_delay *= 2
if retry_delay > RETRY_TIMEOUT:
retry_delay *= retry_config["retry_backoff_factor"]
if retry_delay > retry_config["retry_timeout"]:
self.io.tool_error("Retry timeout exceeded on empty response.")
break

Expand All @@ -2866,10 +2855,13 @@ async def format_in_executor():
exhausted = True
break

should_retry = ex_info.retry
retry_config = models.parse_retry_config(self.get_active_model().retries)

should_retry, _ = models.parse_model_error(retry_config, err)

if should_retry:
retry_delay *= 2
if retry_delay > RETRY_TIMEOUT:
retry_delay *= retry_config["retry_backoff_factor"]
if retry_delay > retry_config["retry_timeout"]:
should_retry = False

if not should_retry:
Expand Down Expand Up @@ -3193,7 +3185,8 @@ def _find_mcp_server_for_tool(self, tool_call):
tool_name_from_schema = nested.getter(tool, "function.name")
if (
tool_name_from_schema
and tool_name_from_schema.lower() == unprefixed_tool_name.lower()
and responses.sanitize_tool_name(tool_name_from_schema).lower()
== responses.sanitize_tool_name(unprefixed_tool_name).lower()
):
# Find the McpServer instance that will be used for communication
for server in self.mcp_manager:
Expand Down Expand Up @@ -3415,6 +3408,10 @@ async def call_mcp_tool_from_session(self, session, tool_call):
# with a "missing required parameter" error.
arguments = responses.coerce_tool_structure(arguments)

# Providers require a provider-safe tool name, so the name coming back
# from the model may not be the name the MCP server advertises.
name = responses.original_tool_name(name)

return await session.call_tool(name=name, arguments=arguments)

async def process_tool_calls(self, tool_call_response):
Expand Down Expand Up @@ -3504,6 +3501,7 @@ def mcp_tools(self, value):

def get_tool_list(self):
"""Get a flattened list of all MCP tools with server prefixes, filtered by registered_servers."""
responses.register_tool_names(self.mcp_tools)
tool_list = []
if self.mcp_tools:
for server_name, server_tools in self.mcp_tools:
Expand Down Expand Up @@ -4877,7 +4875,7 @@ async def allowed_to_edit(self, path):
return

if not Path(full_path).exists():
rel_path = os.path.relpath(full_path)
rel_path = safe_relpath(full_path)
if not await self.io.confirm_ask(f"Create new file? ({rel_path})", subject=path):
self.io.tool_output(f"Skipping edits to {path}")
return
Expand Down
4 changes: 2 additions & 2 deletions cecli/commands/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
parse_quoted_filenames,
quote_filename,
)
from cecli.utils import is_image_file, run_fzf
from cecli.utils import is_image_file, run_fzf, safe_relpath


class AddCommand(BaseCommand):
Expand Down Expand Up @@ -67,7 +67,7 @@ async def execute(cls, io, coder, args, **kwargs):
io.tool_output(f"You can add to git with: /git add {fname}")
continue

confirm_fname = os.path.relpath(fname)
confirm_fname = safe_relpath(fname)
if len(confirm_fname) > 64:
confirm_fname = f".../{os.path.basename(confirm_fname)}"

Expand Down
51 changes: 0 additions & 51 deletions cecli/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,57 +92,6 @@ def __init__(
# Commands that should NOT trigger auto-processing of the queue
self._MANAGEMENT_COMMANDS = {"queue", "list-queue", "remove-queue"}

# ── Queue Management Methods (CLI-33) ────────────────────────────── #
#
# The prompt queue itself lives on the coder (Coder.prompt_queue) and
# is managed by cecli.helpers.command_queue. These thin wrappers keep
# the /queue, /list-queue and /remove-queue command implementations
# stable while operating on the coder that owns this Commands
# instance, so each sub-agent's commands manage that sub-agent's own
# queue.

@property
def prompt_queue(self):
"""Proxy to the owning coder's prompt queue."""
coder = self.coder
return coder.prompt_queue if coder is not None else []

def _insert_prompt(self, text: str, index: int) -> dict:
"""Insert a prompt at the given index in the active coder's queue."""
from cecli.helpers import command_queue

return command_queue.insert_prompt(self._active_coder(), text, index)

def _enqueue_prompt(self, text: str) -> dict:
"""Add a prompt to the owning coder's queue."""
from cecli.helpers import command_queue

return command_queue.enqueue_prompt(self.coder, text)

def _dequeue_prompt(self) -> dict | None:
"""Remove and return the first item from the owning coder's queue."""
from cecli.helpers import command_queue

return command_queue.dequeue_prompt(self.coder)

def _get_queue_length(self) -> int:
"""Return the current number of items in the owning coder's queue."""
from cecli.helpers import command_queue

return command_queue.get_queue_length(self.coder)

def _remove_from_queue(self, index: int) -> dict | None:
"""Remove and return the item at the given index from the owning coder's queue."""
from cecli.helpers import command_queue

return command_queue.remove_from_queue(self.coder, index)

def _clear_queue(self) -> list:
"""Remove all items from the owning coder's queue and return them."""
from cecli.helpers import command_queue

return command_queue.clear_queue(self.coder)

def _load_custom_commands(self, custom_commands):
"""
Load custom commands from plugin paths.
Expand Down
3 changes: 2 additions & 1 deletion cecli/commands/insert_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from cecli.commands.utils.base_command import BaseCommand
from cecli.commands.utils.helpers import format_command_result
from cecli.helpers import command_queue


class InsertQueueCommand(BaseCommand):
Expand Down Expand Up @@ -57,7 +58,7 @@ async def execute(cls, io, coder, args, **kwargs):

# Happy path: insert the prompt
try:
item = coder.commands._insert_prompt(prompt_text, index)
item = command_queue.insert_prompt(coder, prompt_text, index)
io.tool_output(f"Prompt inserted at position {index + 1} (id: {item['id']})")
return f"Successfully executed {cls.NORM_NAME}."
except ValueError as e:
Expand Down
3 changes: 2 additions & 1 deletion cecli/commands/list_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from cecli.commands.utils.base_command import BaseCommand
from cecli.commands.utils.helpers import format_command_result
from cecli.helpers import command_queue


class ListQueueCommand(BaseCommand):
Expand All @@ -30,7 +31,7 @@ async def execute(cls, io, coder, args, **kwargs):
io, cls.NORM_NAME, "", error="Command system not available. Cannot list queue."
)

queue = coder.commands.prompt_queue
queue = command_queue.list_queue(coder)

# Sad path: empty queue
if not queue:
Expand Down
Loading
Loading