From 76933b044c0dc174099ae75a262d9016353d7474 Mon Sep 17 00:00:00 2001 From: worldmozara Date: Tue, 22 Sep 2026 23:17:53 +0800 Subject: [PATCH] docs: describe grep_files pattern and path shapes --- AGENTS.md | 2 +- src/plyngent/agent/directive_checkpoint.py | 2 ++ src/plyngent/config/models.py | 4 ++++ src/plyngent/tools/file/grep_files.py | 18 ++++++++++++++++-- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ae01b96..4c0064c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -92,7 +92,7 @@ Module-level `@tool` handlers. Call `set_workspace_root()` before use. - **`workspace`**: path resolve under primary root **or** temporary allowlist roots **or** a directory-access grant (mode-aware: `read` < `write` < `exec`, default required `write`; a file grant matches only that file); path substring denylist (wins over grants); command basename denylist (CLI: timed human allow override, default deny on timeout; **not** skipped by YOLO); escape error hints `request_directory_access`. - **`access`** (`tools/access.py`): `request_directory_access` (LOCAL|INSTANCE|SESSION) asks the host hook for out-of-workspace access; grants are one `path → mode` pair in one of three stores — `WorkspacePolicy.config_allow` (static `[agent].allow_paths`), `SessionState.access_grants` (human-approved, persisted on the session row), `WorkspacePolicy.yolo_allow` (`--yes`/`/yolo`, process only). `AccessDecision(mode, persist)` from the hook; denylisted paths can never be granted; cap `MAX_ACCESS_GRANTS`. Not a sandbox: `exec` grants (and any command) reach the whole FS. -- **`file`**: `read_file` (`with_lineno`, `max_chars` default 32k; results start with a 1-based `L{begin}-{end}` range), `write_file`, `listdir`, `tree` (model picks `format=markdown` default / `flat` paths / `decorated`; VCS + default noise dirs + optional `skip_dirs` / denylist walk), `glob_paths`, `grep_files` (regex, skip VCS/binary), `edit_replace` (`max_replaces`, reports remaining matches), `edit_lineno` (1-based range; only lines read via `with_lineno`; read state resets on any file write/edit/copy/move/delete), `copy_path` / `move_path` / `delete_path`, `new_temporary_workspace` (system temp allowlist; cleanup on chat exit; no session rebind), `get_truncated` (resume any truncated result via its `truncate_token=...`). +- **`file`**: `read_file` (`with_lineno`, `max_chars` default 32k; results start with a 1-based `L{begin}-{end}` range), `write_file`, `listdir`, `tree` (model picks `format=markdown` default / `flat` paths / `decorated`; VCS + default noise dirs + optional `skip_dirs` / denylist walk), `glob_paths`, `grep_files` (Python `re` per line, not a glob; literal file/dir `path`, no glob expansion; skips VCS/binary), `edit_replace` (`max_replaces`, reports remaining matches), `edit_lineno` (1-based range; only lines read via `with_lineno`; read state resets on any file write/edit/copy/move/delete), `copy_path` / `move_path` / `delete_path`, `new_temporary_workspace` (system temp allowlist; cleanup on chat exit; no session rebind), `get_truncated` (resume any truncated result via its `truncate_token=...`). - **`process`**: `run_argv` (argv, no shell, timeout, optional stdin/env); `run_argv_batch` (serial steps, `pipe_out` on provider, `mix_stderr`, `stop_on_error`); PTY `open_pty` / `read_pty` / `write_pty` (literal) / `write_pty_keys` (escapes) / `ask_into_pty` (human→PTY only; `secret` no-echo; answer never in tool result) / `close_pty` (POSIX: `pty`+`fork`; Windows: ConPTY via `pywinpty` env marker dep). - PTY: backend in `pty_backend.py`; structured status (`alive`/`exit_code`/`data`); `read_pty(..., until=)` (**CSI sanitized** so host TTY is never reset); keys via `write_pty_keys` only (`\xHH`, `ctrl+x`, `key=esc`); secrets via `ask_into_pty` (not `write_pty` data); session limit/idle TTL/output budget; close terminate→kill. - CLI limit hooks: interactive confirm to raise tool-loop rounds, PTY session cap, or PTY output budget. The prompt accepts `y`/empty (once), `n`, or `yyy` = stop asking for the rest of the turn (`cli.limits` module flag, reset by `run_turn_with_retries`, `/btw`, session switches, and chat exit). `--auto-continue` / `[agent] auto_continue_limits` set the process-wide default (covers one-shot/PTY); the flag does not imply YOLO and vice versa. diff --git a/src/plyngent/agent/directive_checkpoint.py b/src/plyngent/agent/directive_checkpoint.py index b06d464..d388ec1 100644 --- a/src/plyngent/agent/directive_checkpoint.py +++ b/src/plyngent/agent/directive_checkpoint.py @@ -22,6 +22,8 @@ use `run_argv_batch` for ordered pipelines. - `edit_replace`: fix match / `max_replaces`; `edit_lineno` edits only lines read via \ `read_file(with_lineno=true)`, and line numbers reset after any file write. - `read_file` results start with `L{begin}-{end}` (1-based lines; offset is 0-based). +- `grep_files`: `pattern` is a Python `re` regex (not a glob, so `*.py` fails) and \ +`path` is a literal file/dir (no glob expansion). - Truncated results carry a `truncate_token=...`; continue with `get_truncated`. - Prefer `fetch` for HTTP(S); private/LAN hosts need human policy allow (not YOLO). - PTY secrets only via `ask_into_pty`; denylists and confirms still apply. diff --git a/src/plyngent/config/models.py b/src/plyngent/config/models.py index 0bbe2de..9707f67 100644 --- a/src/plyngent/config/models.py +++ b/src/plyngent/config/models.py @@ -43,6 +43,10 @@ re-read with line numbers after any `edit_*` / `write_file` / copy/move/delete, since line numbers go stale. - `read_file` results start with a 1-based line range `L{begin}-{end}` (`offset` \ is 0-based); `with_lineno` shows per-line numbers instead. +- `grep_files` takes a Python `re` regex for `pattern`, not a filename glob \ +(`*.py` / `**/*.py` fail with `invalid regex`), and a literal file/dir `path` \ +(no glob expansion, so `src/**/*.py` fails with `path does not exist`); there is \ +no file-type filter, so narrow with `path` and filter hits yourself. - Truncated results (any tool) carry a `truncate_token=...`; use `get_truncated` \ with it to keep reading (chains through truncations). diff --git a/src/plyngent/tools/file/grep_files.py b/src/plyngent/tools/file/grep_files.py index 0f4e675..9ab9342 100644 --- a/src/plyngent/tools/file/grep_files.py +++ b/src/plyngent/tools/file/grep_files.py @@ -140,8 +140,22 @@ async def grep_files( ) -> str: """Search file contents under the workspace with a regular expression. - Returns ``path:line: content`` lines (paths relative to workspace root). - Skips VCS/hidden dirs by default and ignores likely-binary files. + ``pattern`` is a **Python** ``re`` regex matched per line (``re.MULTILINE``), + **not** a filename glob — ``*.py`` / ``**/*.py`` are invalid regexes and fail + with ``error: invalid regex``. There is no file-type filter either: narrow with + ``path`` (a file or a directory) and filter unrelated hits yourself. + ``case_insensitive`` (or an inline ``(?i)``) ignores case. + + ``path`` is a **literal** file or directory (default ``.`` = workspace root); + globs are not expanded, so ``src/**/*.py`` fails with ``error: path does not + exist`` — pass ``src`` instead. + + Returns ``:: `` lines (workspace-relative, absolute outside + the workspace), long lines clipped with ``…``, capped at ``max_matches`` + (default 100, plus a ``...[truncated at N matches]`` marker). VCS/hidden dirs are + skipped unless ``skip_hidden_dirs=False``; files over 1 MB and likely-binary + files are ignored. Returns ``(no matches)`` when nothing matched; an invalid + ``pattern`` or a missing ``path`` returns an ``error: ...`` line. """ if max_matches < 1: return "error: max_matches must be >= 1"