diff --git a/app/tools/plugins/shell_exec/__init__.py b/app/tools/plugins/shell_exec/__init__.py index f608d3b..dc65d57 100644 --- a/app/tools/plugins/shell_exec/__init__.py +++ b/app/tools/plugins/shell_exec/__init__.py @@ -24,10 +24,12 @@ class Tool(BaseTool): stdin_data=str(stdin_secret) if stdin_secret is not None else None, ) output = completed.stdout if completed.returncode == 0 else completed.stderr or completed.stdout + grep_no_matches = "grep" in command and completed.returncode == 1 and not completed.stderr + ok = completed.returncode == 0 or grep_no_matches return ToolResult( tool=self.name, - ok=completed.returncode == 0, + ok=ok, output=output, - error=None if completed.returncode == 0 else f"Command failed with exit code {completed.returncode}", - metadata={"exit_code": completed.returncode}, - ) \ No newline at end of file + error=None if ok else f"Command failed with exit code {completed.returncode}", + metadata={"exit_code": completed.returncode, "no_matches": grep_no_matches}, + ) diff --git a/app/tools/shell_exec.py b/app/tools/shell_exec.py index 364527a..5b8da9e 100644 --- a/app/tools/shell_exec.py +++ b/app/tools/shell_exec.py @@ -29,6 +29,8 @@ class ShellExecTool(BaseTool): ) output = completed.stdout if completed.returncode == 0 else completed.stderr or completed.stdout error_output = completed.stderr or completed.stdout + grep_no_matches = "grep" in command and completed.returncode == 1 and not completed.stderr + ok = completed.returncode == 0 or grep_no_matches is_sudo_error = ( completed.returncode != 0 and @@ -40,8 +42,8 @@ class ShellExecTool(BaseTool): return ToolResult( tool=self.name, - ok=completed.returncode == 0, + ok=ok, output=output, - error=None if completed.returncode == 0 else f"Command failed with exit code {completed.returncode}", - metadata={"exit_code": completed.returncode, "needs_sudo": is_sudo_error}, + error=None if ok else f"Command failed with exit code {completed.returncode}", + metadata={"exit_code": completed.returncode, "needs_sudo": is_sudo_error, "no_matches": grep_no_matches}, ) diff --git a/config/prompts/thinker.md b/config/prompts/thinker.md index cd6aad2..679d89d 100644 --- a/config/prompts/thinker.md +++ b/config/prompts/thinker.md @@ -14,6 +14,9 @@ INSTRUCTIONS: 4. If the user asks about the current local machine, filesystem, processes, packages, logs, runtime state, or anything that must be observed rather than answered from general knowledge, use an appropriate tool. +5. For exploratory tasks, prefer one robust inspection command over many brittle + dependent checks. Missing optional files should be treated as information, not + as a fatal failure. MODE: {mode_hint} - If mode is "execution": create a plan with TOOL STEPS (shell_exec, file_write, etc) diff --git a/tests/test_tools_flow.py b/tests/test_tools_flow.py index 5cc781d..039db28 100644 --- a/tests/test_tools_flow.py +++ b/tests/test_tools_flow.py @@ -112,6 +112,23 @@ def test_shell_exec_allows_safe_command(tmp_path: Path) -> None: assert str(tmp_path) in result["result"]["output"] +def test_shell_exec_treats_grep_no_matches_as_information(tmp_path: Path) -> None: + _write_config_tree(tmp_path) + controller = RuntimeController(base_dir=tmp_path) + result = controller.handle_task( + UserTask( + input="run grep with no matches", + context={ + "requested_tool": "shell_exec", + "tool_args": {"command": "printf 'abc\\n' | grep definitely_missing"}, + }, + ) + ) + assert result["status"] == "completed" + assert result["result"]["metadata"]["exit_code"] == 1 + assert result["result"]["metadata"]["no_matches"] is True + + def test_permission_resolution_can_resume_task(tmp_path: Path) -> None: _write_config_tree(tmp_path) controller = RuntimeController(base_dir=tmp_path)