feat(tools): grep, glob, multi_edit + bash hardening (output cap, configurable timeout)#82
Merged
Conversation
First slice of PRD #76. Adds a pure-Go grep tool the agent can call instead of shelling out to ripgrep/grep, plus the deep-module package that owns the workspace walk semantics so future search tools (#80 glob) can share them. **pkg/search.** Self-contained, dependency-free of tool/agent layers. Content(ctx, root, opts) walks root, applies a default skip list (.git, node_modules, vendor, dist, build, target), sniffs the leading 512 bytes for a NUL byte to skip binaries, scans surviving files line by line with a compiled regex, and returns matches sorted by (path, line) for deterministic ordering. Path/glob filters and a head limit are honored at the package layer; an absolute or "../" path that escapes root is rejected with an explicit error so the tool layer inherits the boundary check for free. **grep tool.** Thin adapter at pkg/agent/tools/search.go. Three output modes: content (default, returns {path, line, text}), files_with_matches (deduped path list), and count (per-file totals). When matches exceed head_limit the response carries Truncated=true plus an Indicator ("showing first N of M …") so the model knows it's seeing a window. Wired into both Prepare (chat) and PrepareCode (code) registries; the existing subagent default-inheritance picks it up automatically. **Config plumbing.** YAML gains a tools.grep block with max_results and max_file_size_bytes; both are optional and zero-valued defaults fall through to the constants in pkg/search. Threaded daemon.Config → agent.Config → tools.SearchConfig. **Tests.** Fixture-workspace coverage in pkg/search for match counts, binary skip, default skip dirs, head-limit truncation, case insensitivity, path scoping, boundary rejection, glob filtering, deterministic ordering, max-file-size, and invalid-pattern. Tool-layer tests cover schema generation (pattern is required), each output mode, invalid output_mode, head-limit + indicator, path-escape rejection, case-insensitive flag, and config-driven MaxResults. Config tests cover the new tools block parsing both present and absent. CODE_AGENT.md gains a Search section pointing at grep.
Second slice of PRD #76. Adds the glob tool the agent can call to find files by doublestar pattern (e.g. **/*_test.go, pkg/**/agent.go), extending pkg/search with a Paths() function that shares the workspace walk semantics introduced for grep in #77. **pkg/search.** Paths(ctx, root, pattern, opts) walks root (or an optional sub-path scope), tests each regular file's workspace-relative slash path against the doublestar pattern, and returns matches with truncation metadata. Reuses Content's default skip-list (.git, node_modules, vendor, dist, build, target) and the same resolveUnderRoot boundary check, so escaping the root is rejected at the package layer. Default sort is mtime-desc (Claude Code convention) with a stable path tiebreaker; SortLex is opt-in. DefaultPathsMaxResults = 1000. **glob tool.** Thin adapter at pkg/agent/tools/search.go. Schema: pattern (required), path (scope), sort (mtime_desc | lex), max_results. Returns {paths, total, returned, truncated, indicator} where indicator ("showing first N of M paths") fires when results are capped. Wired into SearchTools alongside grep so chat, code, and subagent registries pick it up automatically. **Tests.** pkg/search covers doublestar correctness (**/*.go vs pkg/*.go, **/*_test.go), mtime-desc default vs lex, max_results truncation, default skip dirs, path scope, absolute/relative boundary rejection, invalid sort, empty/invalid pattern. Tool-layer covers schema (pattern required), default sort, lex sort, truncation + indicator, path-escape rejection, and invalid sort. CODE_AGENT.md gains a glob bullet under Search and notes preferring it over recursive list_files.
Third slice of PRD #76. Adds multi_edit so the agent can apply an ordered batch of `{old_string, new_string, replace_all?}` edits to a single file in one round-trip, instead of N sequential update_file hops. **multi_edit.** Inlined in pkg/agent/tools/files.go next to update_file (which is left unchanged). Validates the batch up-front (non-empty, each old != new), reads the file once, applies edits sequentially against the in-progress buffer so a later edit can target text produced by an earlier one, and writes the final buffer in a single fs write — atomic by construction. Per-edit replace_all overrides the default unique-old_string requirement. Errors identify the failing edit by index ("edit 1: ...") so the agent can fix one edit and retry without rebuilding the whole batch. **Wiring.** Code mode registers multi_edit via FileTools (now 5 tools). Chat mode registers a thin MultiEditTools(workDir) constructor that returns just the multi_edit tool — chat mode doesn't have read_file etc. so we expose only the edit primitive, not the full file surface. Subagents inherit it automatically through the existing default-inheritance filter. **Tests.** Cover the schema (path + edits required), sequential application, later-targets-earlier-output, per-edit replace_all, atomic-failure-leaves-file-unchanged with per-index error message, non-unique without replace_all, in-progress-buffer uniqueness (first edit creates a duplicate, second edit must fail), empty edits, identical old/new, and missing file. CODE_AGENT.md gains a multi_edit bullet under File operations and a workflow note preferring it over multiple update_file calls when editing the same file two-or-more times.
) Fourth slice of PRD #76. Closes the bash-hardening half of the PRD: the agent can now give long-running commands an explicit timeout budget, and a runaway `find /` or verbose build can no longer dump 100MB into the LLM context. **pkg/exec/bounded.** New deep module wrapping os/exec. `Run(ctx, name, args, opts) (Result, error)` enforces a per-call timeout via context.WithTimeout, and captures stdout/stderr through head+tail buffers — a fixed-size head, a ring-buffered tail, and a total-bytes counter that keeps incrementing past the cap. When total exceeds head+tail the rendered output is line-aligned (head trimmed back to the last newline; tail advanced past the first) with a `[... truncated N bytes from middle ...]` marker between them, so the final lines of a build (which usually contain the actual error) survive. Result carries `Stdout`, `Stderr`, `ExitCode`, `TimedOut`, `StdoutTotalBytes`, `StderrTotalBytes`. Timeout > MaxTimeout returns a clear "exceeds maximum" error rather than being silently clamped; context-deadline kills surface as `TimedOut: true` + `ExitCode: -1`, distinct from a normal failure. **bash tool.** Wired through bounded.Run. New `timeout_seconds` arg (default 30, hard-cap 600 unless cfg overrides). Output JSON gains `timed_out`, `stdout_total_bytes`, `stderr_total_bytes`. Existing calls without the arg keep working unchanged. node tool intentionally left on the legacy exec path — that's slice #81's job — but its constructor signature widens to take ExecConfig so the conversion is a one-file change. **Config plumbing.** YAML gains a `tools.bash` block with `max_timeout_seconds`, `head_bytes`, `tail_bytes`. All optional; zero-valued defaults fall through to the constants in pkg/exec/bounded. Threaded daemon.Config → agent.Config → tools.ExecConfig. **Tests.** pkg/exec/bounded covers small output (no marker), large output (cap + marker + total bytes), timeout (TimedOut + fast return), nonzero exit code propagation, head+tail line alignment around the marker, exact-boundary no-truncation, and independent stderr capping. Also unit-tests the headTailBuf directly. Tool-layer tests cover default-timeout behavior, `timeout_seconds` firing, hard-cap rejection, and large-stdout truncation. TestBash now skips cleanly when bash is absent, matching the existing TestNode pattern. CODE_AGENT.md gains a note on `timeout_seconds`, the truncation marker, and the `stdout_total_bytes` field.
Fifth and final slice of PRD #76. Brings node to bash parity: same timeout_seconds arg (default 30, hard-cap 600), same head+tail output truncation with line-aligned [... truncated N bytes from middle ...] marker, same {timed_out, stdout_total_bytes, stderr_total_bytes} fields in the result. node piggybacks on the existing tools.bash.* config knobs rather than introducing a parallel block — they're the same execution surface, two thin command wrappers over bounded.Run. The legacy exec path (manual context.WithTimeout + bytes.Buffer + exec.ExitError unwrap) is gone; newNodeTool is now the same shape as newBashTool. Existing node calls without timeout_seconds keep working unchanged. CODE_AGENT.md's "Bash" section becomes "Bash and node", since the timeout/truncation contract is identical. Tests mirror bash: default-timeout sanity, timeout_seconds firing (setTimeout 5s killed at 1s, fast return + TimedOut true + exit -1), hard-cap rejection, and large-stdout truncation (200 lines through HeadBytes=100/TailBytes=100, marker present, last line survives in tail). All under t.Run subtests; the existing TestNode skip-when-node- absent guard covers the new cases too.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements PRD #76: Coding-table-stakes tools: grep, glob, multi_edit + bash hardening (output cap, configurable timeout)
Resolved sub-issues
Closes #76