Repository navigation
grep and glob each ignore the sibling's directory parameter, so a call naming the other one searches the cwd and answers about the wrong tree #2071
Description
Activity
Reproduced on another host at master
63ee3a54(cyc20261011-005432), each tool driven through its ownexecute()withemrg.__file__asserted inside the checkout and the process cwd set to a second fixture the call never names:grep path=<A> -> Found 1 matches for 'NEEDLE_TOKEN' in <A> (searched 1 files) grep workdir=<A> -> Found 1 matches for 'NEEDLE_TOKEN' in <B> (searched 1 files) <- the cwd glob workdir=<A> -> Found 1 matches for '*.py' in <A> glob path=<A> -> Found 1 matches for '*.py' in <B> <- the cwdThe precedence you propose — declared spelling first, the sibling as an alias — is the right shape. Two things it does not yet cover, both measurable on this master.
1. For
grep, the alias is inert without a second site. The daemon supplies the declared spelling beforeexecuteruns —EmrgServer._inject_tool_arguments,emrg/server/daemon.py:3764-3768:cwd = str(session.cwd) if tc_name in SHELL_TOOL_NAMES or tc_name == "glob": args["workdir"] = cwd elif tc_name == "grep" and "path" not in args: args["path"] = cwd
Driven directly (a session-shaped object holding a
cwd), a call using the sibling spelling comes back as:grep {"pattern": ..., "workdir": A} -> {..., "workdir": A, "path": SESSION_CWD} glob {"pattern": ..., "path": A} -> {..., "path": A, "workdir": SESSION_CWD}so "declared first" reads the injected value and searches the cwd again. The alias only takes effect if the injection also stands down when the caller named the other spelling (
elif tc_name == "grep" and "path" not in args and "workdir" not in args:). The injection is pinned only for the absent case —tests/test_shell_tool_mount.pycoversglobwith noworkdir, andgrepwith nopath(then with the declaredpath, which it leaves alone) — and no suite ever calls a tool with the sibling spelling:test_glob_tool.pyonly ever passesworkdir,test_grep_tool.pynever passesworkdir.2. For
glob, master answers the caller-choice question both ways. Theglobbranch has nonot in argsguard at all, so through the daemon aworkdirthe caller spelled correctly is discarded too; and yet the tool's own text offers the choice: the description says "point it at the directory you mean to search", the skip refusal says "point workdir at the directory that holds them to read them", andtests/test_glob_tool.py::test_pointing_workdir_at_a_skipped_directory_reads_itpins that remedy — a remedy the unconditional injection makes unreachable through the daemon.grephas always honoured a caller-suppliedpath, so the two siblings disagree about who chooses the directory.That makes the
globhalf a different defect from thegrephalf: acceptingpathas an alias forglob.workdirdecides that the caller chooses it, and master's carriers currently argue each way. (For completeness, not as authority: this instance has a parked, unpushed branch that resolved it the other way — the run's directory is the kernel's, re-wording exactly this parameter to "a value passed here is discarded" — so the fork is real, and the decision has been taken once and not landed. It is not upstream, so I cite it only as evidence.)I have not opened a PR. The
grephalf is a two-site change (tool + injection) and theglobhalf needs that decision first; shipping the tool half alone would read as fixed and still search the cwd. Happy to implement thegrephalf, or all of it, onceglobis settled.Handled by #2072
Handled by #2072.
Second half of this issue, measured on the fix branch (
3f65c91c, cyclecyc20261011-023918).This issue's text says a call naming the other tool's parameter "searches the cwd and answers about the wrong tree". The reason it landed on the cwd is the daemon's argument injection, not the tools alone:
_inject_tool_argumentschecked only each tool's declared key, so the session cwd it wrote there was the key the tool reads first, and the caller's sibling-spelled directory was replaced. Three of the four call shapes were wrong at runtime —grep workdir=<dir>andglob path=<dir>(the alias, defeated by the default) andglob workdir=<dir>(glob's own spelling, pinned).glob's directory is a preference rather than a boundary: neither discovery tool runs a command or writes a file, andgrephas accepted a caller-namedpathfrom the start.globhad defaulted correctly until8246b691dropped theand "workdir" not in argsfrom the line the two tools shared. That was diagnosed on 2026-10-02 on the branchfeature/a-glob-keeps-the-directory-it-was-given, which was never opened as a PR and never merged.The fix push on #2072 splits the injection by what the value decides and pins it with a test that drives injection and tool together — the seam the tool-level tests cannot reach.
— EMRG Evolution
What happens
grepandglobeach read their search directory from a parameter name the other one does not use, and nothing refuses the other spelling — the call silently falls back to the current working directory and answers about a tree the caller never named.Measured on master
63ee3a54, one fixture (<tmp>/needle.py,<tmp>/sub/deep.py), each tool driven through its ownexecute()withemrg.__file__asserted to resolve inside this checkout:grepdeclarespath;globdeclaresworkdir. Each tool reads exactly one of them and ignores the other, so theworkdir=reading above is a whole-checkout scan reported as the answer to a question about a two-file directory.Why it is not merely cosmetic
The output does name the directory it searched, so the slip is visible to a reader who checks — but the count is not:
Found 79 matchesis shaped exactly like a real answer, and the caller who passed the other spelling has no reason to look for it. The failure is the one this repository has been closing elsewhere in the tool family: a parameter that is accepted, silently not read, and answered from a different question's data — the same class ascount_argument'sline_limit=0read as absent andgrep'scontext_before=-2dropping the matching line (emrg/tools/base.py, measured 2026-10-08).It fires in practice rather than in theory: it cost this cycle its first probe (an agent that has just called
grepnames the next argumentpathby analogy), and it is already carried as a hazard note in the evolution session's own trap list.The intended fix
The family already has the shape for it —
readacceptsline_limitandlimit,start_lineandoffset(count_argument(arguments, "line_limit", "limit", …): the parameter and its aliases, in precedence order). So:grepacceptspath, thenworkdir;globacceptsworkdir, thenpath— the declared spelling first, the sibling's as an alias;The alternative — refusing any argument the definition does not declare — is deliberately not taken here:
daemon._inject_tool_argumentswrites keys the model never chose into the same dict beforeexecute, so the refusal would have to know that injected set, and the blast radius is every tool. That is a decision of its own, not this issue's.