diff --git a/plugins/codeact/instructions/codeact.instructions.md.tmpl b/plugins/codeact/instructions/codeact.instructions.md.tmpl index 689ea82..2ae88e4 100644 --- a/plugins/codeact/instructions/codeact.instructions.md.tmpl +++ b/plugins/codeact/instructions/codeact.instructions.md.tmpl @@ -10,7 +10,7 @@ cross-references file sets, or needs ≥5 sequential tool calls. ### Invoke ```bash -{{CODEACT_DIR}}/scripts/codeact --auto --workspace . --code '' +{{CODEACT_DIR}}/scripts/codeact --auto --raw --workspace . --code '' ``` ### Critical rules @@ -25,10 +25,18 @@ cross-references file sets, or needs ≥5 sequential tool calls. - `mcp_call` → **string** - `bash` → **dict** with stdout/stderr/returncode 3. **No `os.path`, no `os.walk`** — use `glob` to find files, `view` to read them. -4. **Wrap file reads in try/except.** Print partial results as you go. -5. **Use double quotes in Python code** to avoid shell quoting conflicts with +4. **`glob` auto-excludes** `.venv`, `node_modules`, `__pycache__`, `.git`, `target`, + `dist`, `build`, and similar directories. If you need files from those dirs, + pass `exclude_dirs=[]` to disable filtering. +5. **Wrap file reads in try/except.** Print partial results as you go. +6. **Use double quotes in Python code** to avoid shell quoting conflicts with `--code '...'`. Write `"string"` not `'string'` inside codeact programs. -6. Skip codeact for ≤5 files or single grep→view→done workflows. +7. Skip codeact for ≤5 files or single grep→view→done workflows. +8. **Keep output minimal.** Only print what the caller actually needs — + summaries, counts, and key findings. Do NOT dump raw file contents or + hundreds of unfiltered lines. If a list could be long, truncate or + aggregate inside the program. The output must fit comfortably in a + single tool response (~5 KB) to avoid extra read round-trips. {{SYNTAX}} diff --git a/plugins/codeact/skills/hyperlight-codeact/scripts/codeact.py b/plugins/codeact/skills/hyperlight-codeact/scripts/codeact.py index a61ce66..00b91ec 100644 --- a/plugins/codeact/skills/hyperlight-codeact/scripts/codeact.py +++ b/plugins/codeact/skills/hyperlight-codeact/scripts/codeact.py @@ -808,6 +808,8 @@ def main() -> None: ap.add_argument("--stack-size", help="Sandbox stack (e.g. '35Mi').") ap.add_argument("--allowed-domains", nargs="*", default=[], help="Domains reachable via sandbox http_get/http_post.") + ap.add_argument("--raw", action="store_true", + help="On success, print stdout/stderr directly instead of JSON envelope.") ap.add_argument("--workspace", help="Restrict file/sql tools to this directory tree.") args = ap.parse_args() @@ -902,7 +904,18 @@ def main() -> None: "success": False, } - print(json.dumps(output, indent=2)) + if args.raw and output["success"]: + sys.stdout.write(output["stdout"]) + if output["stderr"]: + sys.stderr.write(output["stderr"]) + sys.exit(0) + elif args.raw and not output["success"]: + if output["stdout"]: + sys.stdout.write(output["stdout"]) + sys.stderr.write(output["stderr"]) + sys.exit(1) + else: + print(json.dumps(output, indent=2)) if __name__ == "__main__": diff --git a/plugins/codeact/skills/monty-codeact/scripts/codeact.py b/plugins/codeact/skills/monty-codeact/scripts/codeact.py index 8ec4ee7..ac20be7 100644 --- a/plugins/codeact/skills/monty-codeact/scripts/codeact.py +++ b/plugins/codeact/skills/monty-codeact/scripts/codeact.py @@ -96,11 +96,16 @@ def discover_tools() -> list[dict[str, Any]]: tools.append({ "name": "glob", "cli_equivalent": "glob", - "description": "Find files matching a glob pattern (max 200).", + "description": "Find files matching a glob pattern. Auto-excludes " + ".venv, node_modules, __pycache__, .git, target, dist, " + "build. Pass exclude_dirs=[] to include everything.", "parameters": { "pattern": {"type": "string", "required": True, "description": "Glob pattern, e.g. '**/*.py'."}, "paths": {"type": "string", "required": False, "default": "."}, + "exclude_dirs": {"type": "array", "required": False, + "description": "Directories to exclude. Defaults to " + "common virtual/build dirs. Pass [] to disable."}, }, "implementation": {"type": "builtin"}, }) @@ -374,8 +379,26 @@ def _edit(path="", old_str="", new_str=""): return f"Edited {path}" -def _glob(pattern="**/*", paths="."): +_DEFAULT_EXCLUDE_DIRS = { + ".venv", "venv", "node_modules", "__pycache__", ".git", + ".tox", ".mypy_cache", ".pytest_cache", ".ruff_cache", + "target", "dist", "build", ".next", ".nuxt", +} + +def _glob(pattern="**/*", paths=".", exclude_dirs=None): base = _check_workspace(Path(paths)) + # Directories to skip unless user explicitly globs into them + skip = _DEFAULT_EXCLUDE_DIRS if exclude_dirs is None else set(exclude_dirs) + + def _should_include(p: Path) -> bool: + if not p.is_file(): + return False + # Skip files inside excluded directories + for part in p.parts: + if part in skip: + return False + return True + # Support brace expansion: {a,b} → run multiple globs and merge if "{" in pattern and "}" in pattern: prefix = pattern[:pattern.index("{")] @@ -386,10 +409,18 @@ def _glob(pattern="**/*", paths="."): expanded = [prefix + alt + suffix for alt in alternatives] all_matches: list[str] = [] for p in expanded: - all_matches.extend(str(m) for m in base.glob(p) if m.is_file()) - matches = sorted(set(all_matches))[:200] + all_matches.extend(str(m) for m in base.glob(p) if _should_include(m)) + matches = sorted(set(all_matches)) else: - matches = sorted(str(p) for p in base.glob(pattern) if p.is_file())[:200] + matches = sorted(str(p) for p in base.glob(pattern) if _should_include(p)) + + # Safety cap at 10000 with a warning if truncated + cap = 10000 + if len(matches) > cap: + print(f"⚠ glob matched {len(matches)} files, returning first {cap}. " + f"Use a more specific pattern.", file=sys.stderr) + matches = matches[:cap] + # Return workspace-relative paths so sandbox code doesn't need to strip prefixes if _WORKSPACE_ROOT is not None: root = str(_WORKSPACE_ROOT) + "/" @@ -717,6 +748,8 @@ def main() -> None: help="Max execution steps (Monty limit).") ap.add_argument("--max-memory", type=int, default=None, help="Max memory in bytes (Monty limit).") + ap.add_argument("--raw", action="store_true", + help="On success, print stdout/stderr directly instead of JSON envelope.") args = ap.parse_args() # ---- discovery / instructions ---- @@ -823,7 +856,18 @@ def print_callback(stream: str, text: str) -> None: "success": False, } - print(json.dumps(output, indent=2, default=str)) + if args.raw and output["success"]: + sys.stdout.write(output["stdout"]) + if output["stderr"]: + sys.stderr.write(output["stderr"]) + sys.exit(0) + elif args.raw and not output["success"]: + if output["stdout"]: + sys.stdout.write(output["stdout"]) + sys.stderr.write(output["stderr"]) + sys.exit(1) + else: + print(json.dumps(output, indent=2, default=str)) if __name__ == "__main__":