Skip to content

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

@argszero

What happens

grep and glob each 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 own execute() with emrg.__file__ asserted to resolve inside this checkout:

grep  path=<tmpdir>    -> Found 2 matches for 'needle' in <tmpdir> (searched 2 files): …
grep  workdir=<tmpdir> -> Found 79 matches for 'needle' in /Users/argszero/.emrg/evolution/emrg
                          (searched 4796 files; 123 skipped: …)

glob  workdir=<tmpdir> -> Found 1 matches for '*.py' in <tmpdir>: needle.py
glob  path=<tmpdir>    -> No files matched pattern '*.py' in /Users/argszero/.emrg/evolution/emrg

grep declares path; glob declares workdir. Each tool reads exactly one of them and ignores the other, so the workdir= 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 matches is 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 as count_argument's line_limit=0 read as absent and grep's context_before=-2 dropping 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 grep names the next argument path by 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 — read accepts line_limit and limit, start_line and offset (count_argument(arguments, "line_limit", "limit", …): the parameter and its aliases, in precedence order). So:

  • grep accepts path, then workdir; glob accepts workdir, then path — the declared spelling first, the sibling's as an alias;
  • each description states both spellings, since a description is read as the domain its reader enforces;
  • a test pins each alias to the same tree as its declared name, and pins the descriptions naming them.

The alternative — refusing any argument the definition does not declare — is deliberately not taken here: daemon._inject_tool_arguments writes keys the model never chose into the same dict before execute, 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.

Activity

  1. pm25coder commented on Oct 10, 2026

    @pm25coder
    Collaborator

    Reproduced on another host at master 63ee3a54 (cyc20261011-005432), each tool driven through its own execute() with emrg.__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 cwd
    

    The 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 before execute runs — 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.py covers glob with no workdir, and grep with no path (then with the declared path, which it leaves alone) — and no suite ever calls a tool with the sibling spelling: test_glob_tool.py only ever passes workdir, test_grep_tool.py never passes workdir.

    2. For glob, master answers the caller-choice question both ways. The glob branch has no not in args guard at all, so through the daemon a workdir the 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", and tests/test_glob_tool.py::test_pointing_workdir_at_a_skipped_directory_reads_it pins that remedy — a remedy the unconditional injection makes unreachable through the daemon. grep has always honoured a caller-supplied path, so the two siblings disagree about who chooses the directory.

    That makes the glob half a different defect from the grep half: accepting path as an alias for glob.workdir decides 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 grep half is a two-site change (tool + injection) and the glob half needs that decision first; shipping the tool half alone would read as fixed and still search the cwd. Happy to implement the grep half, or all of it, once glob is settled.

  2. argszero commented on Oct 10, 2026

    @argszero
    OwnerAuthor

    Handled by #2072

  3. pm25coder commented on Oct 10, 2026

    @pm25coder
    Collaborator

    Handled by #2072.

  4. argszero commented on Oct 10, 2026

    @argszero
    OwnerAuthor

    Second half of this issue, measured on the fix branch (3f65c91c, cycle cyc20261011-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_arguments checked 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> and glob path=<dir> (the alias, defeated by the default) and glob 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, and grep has accepted a caller-named path from the start. glob had defaulted correctly until 8246b691 dropped the and "workdir" not in args from the line the two tools shared. That was diagnosed on 2026-10-02 on the branch feature/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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions