aru-code 0.24.1__tar.gz → 0.25.0__tar.gz

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 (69) hide show
  1. {aru_code-0.24.1/aru_code.egg-info → aru_code-0.25.0}/PKG-INFO +6 -4
  2. {aru_code-0.24.1 → aru_code-0.25.0}/README.md +5 -3
  3. aru_code-0.25.0/aru/__init__.py +1 -0
  4. {aru_code-0.24.1 → aru_code-0.25.0}/aru/agents/base.py +9 -10
  5. {aru_code-0.24.1 → aru_code-0.25.0}/aru/agents/explorer.py +16 -14
  6. {aru_code-0.24.1 → aru_code-0.25.0}/aru/agents/planner.py +7 -3
  7. {aru_code-0.24.1 → aru_code-0.25.0}/aru/display.py +36 -4
  8. {aru_code-0.24.1 → aru_code-0.25.0}/aru/runner.py +87 -6
  9. {aru_code-0.24.1 → aru_code-0.25.0}/aru/tools/codebase.py +659 -207
  10. {aru_code-0.24.1 → aru_code-0.25.0}/aru/tools/gitignore.py +76 -11
  11. {aru_code-0.24.1 → aru_code-0.25.0}/aru/tools/ranker.py +83 -16
  12. {aru_code-0.24.1 → aru_code-0.25.0/aru_code.egg-info}/PKG-INFO +6 -4
  13. {aru_code-0.24.1 → aru_code-0.25.0}/pyproject.toml +1 -1
  14. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_codebase.py +68 -101
  15. aru_code-0.24.1/aru/__init__.py +0 -1
  16. {aru_code-0.24.1 → aru_code-0.25.0}/LICENSE +0 -0
  17. {aru_code-0.24.1 → aru_code-0.25.0}/aru/agent_factory.py +0 -0
  18. {aru_code-0.24.1 → aru_code-0.25.0}/aru/agents/__init__.py +0 -0
  19. {aru_code-0.24.1 → aru_code-0.25.0}/aru/agents/executor.py +0 -0
  20. {aru_code-0.24.1 → aru_code-0.25.0}/aru/cache_patch.py +0 -0
  21. {aru_code-0.24.1 → aru_code-0.25.0}/aru/checkpoints.py +0 -0
  22. {aru_code-0.24.1 → aru_code-0.25.0}/aru/cli.py +0 -0
  23. {aru_code-0.24.1 → aru_code-0.25.0}/aru/commands.py +0 -0
  24. {aru_code-0.24.1 → aru_code-0.25.0}/aru/completers.py +0 -0
  25. {aru_code-0.24.1 → aru_code-0.25.0}/aru/config.py +0 -0
  26. {aru_code-0.24.1 → aru_code-0.25.0}/aru/context.py +0 -0
  27. {aru_code-0.24.1 → aru_code-0.25.0}/aru/history_blocks.py +0 -0
  28. {aru_code-0.24.1 → aru_code-0.25.0}/aru/permissions.py +0 -0
  29. {aru_code-0.24.1 → aru_code-0.25.0}/aru/plugins/__init__.py +0 -0
  30. {aru_code-0.24.1 → aru_code-0.25.0}/aru/plugins/custom_tools.py +0 -0
  31. {aru_code-0.24.1 → aru_code-0.25.0}/aru/plugins/hooks.py +0 -0
  32. {aru_code-0.24.1 → aru_code-0.25.0}/aru/plugins/manager.py +0 -0
  33. {aru_code-0.24.1 → aru_code-0.25.0}/aru/plugins/tool_api.py +0 -0
  34. {aru_code-0.24.1 → aru_code-0.25.0}/aru/providers.py +0 -0
  35. {aru_code-0.24.1 → aru_code-0.25.0}/aru/runtime.py +0 -0
  36. {aru_code-0.24.1 → aru_code-0.25.0}/aru/session.py +0 -0
  37. {aru_code-0.24.1 → aru_code-0.25.0}/aru/tools/__init__.py +0 -0
  38. {aru_code-0.24.1 → aru_code-0.25.0}/aru/tools/ast_tools.py +0 -0
  39. {aru_code-0.24.1 → aru_code-0.25.0}/aru/tools/mcp_client.py +0 -0
  40. {aru_code-0.24.1 → aru_code-0.25.0}/aru/tools/tasklist.py +0 -0
  41. {aru_code-0.24.1 → aru_code-0.25.0}/aru_code.egg-info/SOURCES.txt +0 -0
  42. {aru_code-0.24.1 → aru_code-0.25.0}/aru_code.egg-info/dependency_links.txt +0 -0
  43. {aru_code-0.24.1 → aru_code-0.25.0}/aru_code.egg-info/entry_points.txt +0 -0
  44. {aru_code-0.24.1 → aru_code-0.25.0}/aru_code.egg-info/requires.txt +0 -0
  45. {aru_code-0.24.1 → aru_code-0.25.0}/aru_code.egg-info/top_level.txt +0 -0
  46. {aru_code-0.24.1 → aru_code-0.25.0}/setup.cfg +0 -0
  47. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_agents_base.py +0 -0
  48. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_checkpoints.py +0 -0
  49. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli.py +0 -0
  50. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli_advanced.py +0 -0
  51. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli_base.py +0 -0
  52. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli_completers.py +0 -0
  53. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli_new.py +0 -0
  54. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli_run_cli.py +0 -0
  55. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli_session.py +0 -0
  56. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_cli_shell.py +0 -0
  57. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_confabulation_regression.py +0 -0
  58. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_config.py +0 -0
  59. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_context.py +0 -0
  60. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_executor.py +0 -0
  61. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_gitignore.py +0 -0
  62. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_guardrails_scenarios.py +0 -0
  63. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_main.py +0 -0
  64. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_mcp_client.py +0 -0
  65. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_permissions.py +0 -0
  66. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_planner.py +0 -0
  67. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_plugins.py +0 -0
  68. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_providers.py +0 -0
  69. {aru_code-0.24.1 → aru_code-0.25.0}/tests/test_ranker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aru-code
3
- Version: 0.24.1
3
+ Version: 0.25.0
4
4
  Summary: A Claude Code clone built with Agno agents
5
5
  Author-email: Estevao <estevaofon@gmail.com>
6
6
  License-Expression: MIT
@@ -53,7 +53,7 @@ An intelligent coding assistant for the terminal, powered by LLMs and [Agno](htt
53
53
 
54
54
  ## Highlights
55
55
 
56
- - **Multi-Agent Architecture** — Specialized agents for planning, execution, and conversation
56
+ - **Multi-Agent Architecture** — Specialized agents for planning, execution, exploration, and conversation
57
57
  - **Interactive CLI** — Streaming responses, multi-line paste, session management
58
58
  - **Image Support** — Attach images via `@` mentions for multimodal analysis (Claude, GPT-4o, Gemini)
59
59
  - **11 Integrated Tools** — File operations, code search, shell, web search, task delegation
@@ -513,12 +513,13 @@ Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
513
513
  | **Planner** | Analyzes codebase, creates structured implementation plans | Read-only tools, search, web |
514
514
  | **Executor** | Implements code changes based on plans or instructions | All tools including delegation |
515
515
  | **General** | Handles conversation and simple operations | All tools including delegation |
516
+ | **Explorer** | Fast, read-only codebase exploration and search | Read-only tools, search, bash (read-only) |
516
517
 
517
518
  ## Tools
518
519
 
519
520
  ### File Operations
520
521
  - `read_file` — Reads files with line range support and binary detection
521
- - `read_file_smart` — Answers specific questions about a file without returning raw content
522
+ - `read_files` — Reads multiple files in parallel (single batched call)
522
523
  - `write_file` — Writes content to files, creating directories as needed
523
524
  - `edit_file` — Find-and-replace edits on files
524
525
 
@@ -553,7 +554,8 @@ aru-code/
553
554
  │ ├── permissions.py # Granular permission system (allow/ask/deny)
554
555
  │ ├── agents/
555
556
  │ │ ├── planner.py # Planning agent
556
- │ │ └── executor.py # Execution agent
557
+ │ │ ├── executor.py # Execution agent
558
+ │ │ └── explorer.py # Explorer agent (fast, read-only codebase search)
557
559
  │ └── tools/
558
560
  │ ├── codebase.py # 11 core tools
559
561
  │ ├── ast_tools.py # Tree-sitter code analysis
@@ -6,7 +6,7 @@ An intelligent coding assistant for the terminal, powered by LLMs and [Agno](htt
6
6
 
7
7
  ## Highlights
8
8
 
9
- - **Multi-Agent Architecture** — Specialized agents for planning, execution, and conversation
9
+ - **Multi-Agent Architecture** — Specialized agents for planning, execution, exploration, and conversation
10
10
  - **Interactive CLI** — Streaming responses, multi-line paste, session management
11
11
  - **Image Support** — Attach images via `@` mentions for multimodal analysis (Claude, GPT-4o, Gemini)
12
12
  - **11 Integrated Tools** — File operations, code search, shell, web search, task delegation
@@ -466,12 +466,13 @@ Aru can load tools from MCP servers. Configure in `.aru/mcp_config.json`:
466
466
  | **Planner** | Analyzes codebase, creates structured implementation plans | Read-only tools, search, web |
467
467
  | **Executor** | Implements code changes based on plans or instructions | All tools including delegation |
468
468
  | **General** | Handles conversation and simple operations | All tools including delegation |
469
+ | **Explorer** | Fast, read-only codebase exploration and search | Read-only tools, search, bash (read-only) |
469
470
 
470
471
  ## Tools
471
472
 
472
473
  ### File Operations
473
474
  - `read_file` — Reads files with line range support and binary detection
474
- - `read_file_smart` — Answers specific questions about a file without returning raw content
475
+ - `read_files` — Reads multiple files in parallel (single batched call)
475
476
  - `write_file` — Writes content to files, creating directories as needed
476
477
  - `edit_file` — Find-and-replace edits on files
477
478
 
@@ -506,7 +507,8 @@ aru-code/
506
507
  │ ├── permissions.py # Granular permission system (allow/ask/deny)
507
508
  │ ├── agents/
508
509
  │ │ ├── planner.py # Planning agent
509
- │ │ └── executor.py # Execution agent
510
+ │ │ ├── executor.py # Execution agent
511
+ │ │ └── explorer.py # Explorer agent (fast, read-only codebase search)
510
512
  │ └── tools/
511
513
  │ ├── codebase.py # 11 core tools
512
514
  │ ├── ast_tools.py # Tree-sitter code analysis
@@ -0,0 +1 @@
1
+ __version__ = "0.25.0"
@@ -66,8 +66,8 @@ Every tool call accumulates its result in your context window. Use the minimum n
66
66
 
67
67
  1. **Find files/patterns** → `grep_search(pattern, file_glob="*.py")` or `glob_search`. \
68
68
  Default shows 10 lines of context — use `context_lines=30` for full function bodies.
69
- 2. **Understand a file** → `read_file_smart(file_path, query)` — returns a concise answer, not raw content
70
- 3. **Need raw content** → `read_file(file_path)` — returns first chunk + outline for large files
69
+ 2. **Need raw content** → `read_file(file_path)` — returns first chunk + outline for large files
70
+ 3. **Need several files at once** → `read_files(paths)` — parallel batch read
71
71
 
72
72
  **Batch independent tool calls**: When you need answers from multiple independent sources, \
73
73
  emit ALL those tool calls in a single response.
@@ -148,10 +148,10 @@ split into subtasks grouped by concern (e.g. "Create model files", "Create route
148
148
 
149
149
  ## Reading strategy — read, edit, test
150
150
 
151
- 1. **Know the file + have a question?** → `read_file_smart(file_path, query)`
152
- 2. **Need a specific pattern?** → `grep_search(pattern, file_glob="*.py")` — default 10 lines context. \
151
+ 1. **Need a specific pattern?** → `grep_search(pattern, file_glob="*.py")` — default 10 lines context. \
153
152
  Use `context_lines=30` for full function bodies.
154
- 3. **Need lines for editing?** → `read_file(file_path, start_line=N, end_line=M)` using line numbers from grep
153
+ 2. **Need lines for editing?** → `read_file(file_path, start_line=N, end_line=M)` using line numbers from grep
154
+ 3. **Need several files at once?** → `read_files(paths)` — parallel batch read
155
155
  4. **Need the whole file?** → `read_file(file_path)` — returns first chunk + outline for large files
156
156
  5. **Need the COMPLETE file (>60KB)?** → `read_file(file_path, max_size=0)` — reads in chunks. Use rarely.
157
157
 
@@ -194,10 +194,9 @@ Skip exploration when the task is clear and the relevant files are obvious.
194
194
 
195
195
  Every tool call accumulates its result in your context window. Use the minimum needed:
196
196
 
197
- 1. **Don't know which file?** → `grep_search` / `glob_search` for patterns, \
198
- `read_file_smart(file_path, query)` when you know the file.
199
- 2. **Know the file + have a question?** → `read_file_smart(file_path, query)`
200
- 3. **Need specific lines?** → `read_file(file_path, start_line=N, end_line=M)`
197
+ 1. **Don't know which file?** → `grep_search` / `glob_search` for patterns.
198
+ 2. **Need specific lines?** → `read_file(file_path, start_line=N, end_line=M)`
199
+ 3. **Need several files at once?** → `read_files(paths)` — parallel batch read.
201
200
  4. **Need the whole file?** → `read_file(file_path)` — returns first chunk + outline for large files.
202
201
 
203
202
  **NEVER read the same file twice.** Check if you already have the content in context.
@@ -218,7 +217,7 @@ For simple, directed lookups (one known file, one specific symbol) use \
218
217
 
219
218
  For **anything broader** — understanding a system, researching before implementing, \
220
219
  analyzing multiple files, writing specs or documentation — **always use explorer agents**. \
221
- Every `read_file` / `read_file_smart` / `grep_search` result you call directly accumulates \
220
+ Every `read_file` / `read_files` / `grep_search` result you call directly accumulates \
222
221
  in YOUR context window and stays there forever. Explorer agents read files in their own \
223
222
  isolated context and return only a concise summary. This is critical: \
224
223
  **3 explorer summaries < 8 raw file reads** in context cost.
@@ -7,24 +7,26 @@ from agno.agent import Agent
7
7
  from aru.providers import create_model
8
8
  from aru.runtime import get_ctx
9
9
  from aru.tools.codebase import (
10
+ _glob_search_tool,
11
+ _grep_search_tool,
12
+ _list_directory_tool,
13
+ _rank_files_tool,
14
+ _read_file_tool,
10
15
  bash,
11
- glob_search,
12
- grep_search,
13
- list_directory,
14
- rank_files,
15
- read_file,
16
- read_file_smart,
16
+ read_files,
17
17
  )
18
18
 
19
- # Read-only tools only — no write/edit/delegate (prevents recursion and mutations)
19
+ # Read-only tools only — no write/edit/delegate (prevents recursion and mutations).
20
+ # All wrappers are async so the Explorer's "multi-parallel tool calls" prompt
21
+ # actually matches runtime behavior — Agno can await them concurrently.
20
22
  EXPLORER_TOOLS = [
21
- read_file,
22
- read_file_smart,
23
- glob_search,
24
- grep_search,
25
- list_directory,
23
+ _read_file_tool,
24
+ read_files,
25
+ _glob_search_tool,
26
+ _grep_search_tool,
27
+ _list_directory_tool,
26
28
  bash,
27
- rank_files,
29
+ _rank_files_tool,
28
30
  ]
29
31
 
30
32
  EXPLORER_ROLE = """\
@@ -52,7 +54,7 @@ Guidelines:
52
54
  - Use glob_search for broad file pattern matching
53
55
  - Use grep_search for searching file contents with regex
54
56
  - Use read_file when you know the specific file path you need to read
55
- - Use read_file_smart when you know the file and have a specific question about it
57
+ - Use read_files (batch) when you need to pull several files at once
56
58
  - Use bash ONLY for read-only operations (ls, git status, git log, git diff, find, cat, head, tail)
57
59
  - NEVER use bash for: mkdir, touch, rm, cp, mv, git add, git commit, npm install, pip install, \
58
60
  or any file creation/modification
@@ -6,7 +6,11 @@ from agno.compression.manager import CompressionManager
6
6
  from aru.agents.base import build_instructions
7
7
  from aru.providers import create_model
8
8
  from aru.tools.codebase import (
9
- glob_search, grep_search, list_directory, read_file, read_file_smart,
9
+ _glob_search_tool,
10
+ _grep_search_tool,
11
+ _list_directory_tool,
12
+ _read_file_tool,
13
+ read_files,
10
14
  )
11
15
  from aru.runtime import get_ctx
12
16
 
@@ -34,8 +38,8 @@ Return ONLY the markdown plan. No explanation, no preamble.\
34
38
 
35
39
  # Planner uses read-only tools only — no write/edit/bash
36
40
  PLANNER_TOOLS = [
37
- read_file, read_file_smart,
38
- glob_search, grep_search, list_directory,
41
+ _read_file_tool, read_files,
42
+ _glob_search_tool, _grep_search_tool, _list_directory_tool,
39
43
  ]
40
44
 
41
45
 
@@ -161,6 +161,10 @@ class StatusBar:
161
161
  self._index = 0
162
162
  self._last_switch = time.monotonic()
163
163
  self._override: str | None = None
164
+ # A single persistent Spinner — Rich's Spinner tracks frames via
165
+ # (time - start_time), so instantiating a new one per render would
166
+ # reset start_time each frame and make the animation look frozen.
167
+ self._spinner = Spinner("dots", text="", style="cyan")
164
168
 
165
169
  @property
166
170
  def current_text(self) -> str:
@@ -187,8 +191,8 @@ class StatusBar:
187
191
 
188
192
  def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
189
193
  self._maybe_rotate()
190
- spinner = Spinner("dots", text=f"[dim]{self.current_text}[/dim]", style="cyan")
191
- yield from spinner.__rich_console__(console, options)
194
+ self._spinner.update(text=Text(self.current_text, style="dim"))
195
+ yield from self._spinner.__rich_console__(console, options)
192
196
 
193
197
  def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
194
198
  return Measurement(1, options.max_width)
@@ -196,7 +200,7 @@ class StatusBar:
196
200
 
197
201
  TOOL_DISPLAY_NAMES = {
198
202
  "read_file": "Read",
199
- "read_file_smart": "ReadSmart",
203
+ "read_files": "ReadBatch",
200
204
  "write_file": "Write",
201
205
  "edit_file": "Edit",
202
206
  "glob_search": "Glob",
@@ -209,7 +213,7 @@ TOOL_DISPLAY_NAMES = {
209
213
 
210
214
  TOOL_PRIMARY_ARG = {
211
215
  "read_file": "file_path",
212
- "read_file_smart": "file_path",
216
+ "read_files": "paths",
213
217
  "write_file": "file_path",
214
218
  "edit_file": "file_path",
215
219
  "glob_search": "pattern",
@@ -253,6 +257,34 @@ def _format_tool_label(tool_name: str, tool_args: dict | None) -> str:
253
257
  return display
254
258
 
255
259
 
260
+ def subagent_progress(label: str, tool_name: str, tool_args: dict | None,
261
+ duration: float | None = None):
262
+ """Print sub-agent tool completion into the active Live context (or console).
263
+
264
+ Only called on tool completion — shows a single ✓ line per tool call,
265
+ keeping the output compact (no duplicate start/complete lines).
266
+ """
267
+ from aru.runtime import get_ctx
268
+ try:
269
+ ctx = get_ctx()
270
+ except LookupError:
271
+ return
272
+ tool_label = _format_tool_label(tool_name, tool_args)
273
+ dur_str = f" {duration:.1f}s" if duration and duration >= 0.5 else ""
274
+ line = Text.assemble(
275
+ (" ", ""),
276
+ ("\u2713 ", "bold green"),
277
+ (f"[{label}] ", "dim"),
278
+ (tool_label, "dim"),
279
+ (dur_str, "dim cyan"),
280
+ )
281
+ target = ctx.live if ctx.live else None
282
+ if target:
283
+ target.console.print(line)
284
+ else:
285
+ ctx.console.print(line)
286
+
287
+
256
288
  class ToolTracker:
257
289
  """Tracks active tool calls with timing, displayed inside the Live area."""
258
290
 
@@ -462,6 +462,24 @@ def _build_file_context(file_paths: list[str], max_total: int = 20_000) -> str:
462
462
  return "## Pre-loaded file contents (do NOT re-read these files)\n\n" + "\n\n".join(parts)
463
463
 
464
464
 
465
+ _MODIFY_VERBS = frozenset({
466
+ "add", "create", "write", "edit", "modify", "update", "implement",
467
+ "fix", "replace", "rename", "move", "refactor", "remove", "delete",
468
+ })
469
+ _MUTATION_LABELS = frozenset({"Write", "Edit", "Bash"})
470
+
471
+
472
+ def _step_expected_mutation(description: str) -> bool:
473
+ """Check if step description implies file modifications."""
474
+ first_word = description.strip().split()[0].lower().rstrip(":")
475
+ return first_word in _MODIFY_VERBS
476
+
477
+
478
+ def _has_mutation_tool(tool_calls: list[str]) -> bool:
479
+ """Check if any tool call is a write/edit/bash operation."""
480
+ return any(tc.split("(")[0] in _MUTATION_LABELS for tc in tool_calls)
481
+
482
+
465
483
  async def execute_plan_steps(session, executor_factory) -> str | None:
466
484
  """Execute plan steps one by one with live progress tracking."""
467
485
  plan_files = _extract_plan_file_paths(session.current_plan)
@@ -586,12 +604,57 @@ async def execute_plan_steps(session, executor_factory) -> str | None:
586
604
  )
587
605
 
588
606
  if step_failed:
589
- step.status = "failed"
607
+ # Auto-retry once with error context before asking the user
590
608
  fail_msg = content[:200] if content else f"{tasks_failed}/{tasks_total} subtasks failed"
591
- console.print(f"\n[red]Step {step.index} failed: {fail_msg}[/red]")
592
- if not get_skip_permissions() and not ask_yes_no("Continue with remaining steps?"):
593
- break
594
- elif content or tasks_all_done:
609
+ console.print(f"\n[yellow]Step {step.index} failed, retrying automatically...[/yellow]")
610
+ reset_task_store()
611
+ retry_prompt = (
612
+ step_prompt
613
+ + f"\n\n## Previous Attempt Failed\nError: {fail_msg}\n"
614
+ + "Fix the issues from the previous attempt. Do NOT repeat the same mistake."
615
+ )
616
+ executor = executor_factory()
617
+ retry_result = await run_agent_capture(executor, retry_prompt, session, lightweight=True)
618
+ content = retry_result.content
619
+ run_result = retry_result
620
+
621
+ # Re-evaluate after retry
622
+ store = get_task_store()
623
+ all_tasks = store.get_all()
624
+ tasks_completed = sum(1 for t in all_tasks if t["status"] == "completed")
625
+ tasks_failed = sum(1 for t in all_tasks if t["status"] == "failed")
626
+ tasks_total = len(all_tasks)
627
+ tasks_all_done = tasks_total > 0 and (tasks_completed + tasks_failed == tasks_total)
628
+
629
+ still_failed = False
630
+ if tasks_all_done and tasks_failed > 0 and tasks_completed == 0:
631
+ still_failed = True
632
+ elif content:
633
+ still_failed = (
634
+ content.startswith("Error")
635
+ or "Error from OpenAI API" in content
636
+ or "Error in Agent run" in content
637
+ )
638
+
639
+ if still_failed:
640
+ step.status = "failed"
641
+ fail_msg = content[:200] if content else f"{tasks_failed}/{tasks_total} subtasks failed"
642
+ console.print(f"\n[red]Step {step.index} failed after retry: {fail_msg}[/red]")
643
+ if not get_skip_permissions() and not ask_yes_no("Continue with remaining steps?"):
644
+ break
645
+ else:
646
+ # Retry succeeded — fall through to success handling
647
+ step_failed = False
648
+
649
+ # Mutation validation warning (non-blocking)
650
+ if not step_failed and not run_result.stalled:
651
+ if _step_expected_mutation(step.description) and not _has_mutation_tool(run_result.tool_calls):
652
+ console.print(
653
+ f"[yellow]\u26a0 Step {step.index} was expected to modify files "
654
+ f"but no write tools were called.[/yellow]"
655
+ )
656
+
657
+ if not step_failed and (content or tasks_all_done):
595
658
  step.status = "completed"
596
659
  summary = content or f"All {tasks_completed} subtasks completed."
597
660
  step_text = f"### Step {step.index}: {step.description}\n{summary}"
@@ -609,8 +672,26 @@ async def execute_plan_steps(session, executor_factory) -> str | None:
609
672
  if not get_skip_permissions() and not ask_yes_no("Continue with remaining steps?"):
610
673
  break
611
674
  except Exception as e:
675
+ # Auto-retry once on exception
676
+ console.print(f"\n[yellow]Step {step.index} error: {e}. Retrying...[/yellow]")
677
+ try:
678
+ reset_task_store()
679
+ retry_prompt = (
680
+ step_prompt
681
+ + f"\n\n## Previous Attempt Error\n{e}\n"
682
+ + "Fix the issues and complete this step."
683
+ )
684
+ executor = executor_factory()
685
+ retry_result = await run_agent_capture(executor, retry_prompt, session, lightweight=True)
686
+ if retry_result.content and not retry_result.content.startswith("Error"):
687
+ step.status = "completed"
688
+ all_results.append(f"### Step {step.index}: {step.description}\n{retry_result.content}")
689
+ completed_context += f"\n- Step {step.index} ({step.description}): Done (after retry)"
690
+ continue
691
+ except Exception:
692
+ pass
612
693
  step.status = "failed"
613
- console.print(f"\n[red]Step {step.index} failed: {e}[/red]")
694
+ console.print(f"\n[red]Step {step.index} failed after retry: {e}[/red]")
614
695
  if not get_skip_permissions() and not ask_yes_no("Continue with remaining steps?"):
615
696
  break
616
697