Skip to content

feat(tools): grep, glob, multi_edit + bash hardening (output cap, configurable timeout)#82

Merged
ludusrusso merged 5 commits into
mainfrom
ralph/76
Apr 30, 2026
Merged

feat(tools): grep, glob, multi_edit + bash hardening (output cap, configurable timeout)#82
ludusrusso merged 5 commits into
mainfrom
ralph/76

Conversation

@ludusrusso

Copy link
Copy Markdown
Owner

Summary

Implements PRD #76: Coding-table-stakes tools: grep, glob, multi_edit + bash hardening (output cap, configurable timeout)

Resolved sub-issues

Closes #76

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.
@ludusrusso ludusrusso added the prd Product Requirements Document label Apr 30, 2026
@ludusrusso ludusrusso changed the title Coding-table-stakes tools: grep, glob, multi_edit + bash hardening (output cap, configurable timeout) feat(tools): grep, glob, multi_edit + bash hardening (output cap, configurable timeout) Apr 30, 2026
@ludusrusso ludusrusso merged commit 960de63 into main Apr 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

prd Product Requirements Document

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Coding-table-stakes tools: grep, glob, multi_edit + bash hardening (output cap, configurable timeout)

1 participant