Skip to content

[bug] A Repo Coder cannot find a file — no search tool, repo_tree stops at 4 levels without saying so, and repo_git's path is ignored by 4 of its 5 commands #508

Description

@serge-ivo

A Repo Coder has no way to FIND a file — and the tool that looks like it can silently ignores the parameter that would

The repo-local connector gives a Repo Coder four read tools: repo_tree, repo_read_file,
repo_git, repo_remote (workers/api/src/lib/connectors/repo-local.ts:120-250, complete list).
None of them searches. There is no grep, no filename match, no content match — grepped for
repo_grep, repo_search, repo_find across the worker and the runner: 0 hits. So locating a
file means walking the tree by hand, and when the tree runs out the model guesses.

What that looks like in one turn

Heartfull (f8ddc272-0390-4826-8812-94989e3d2ebd, agent coder-repo), 2026-08-11 22:28:39,
answering "Check why host role can't manage events via portal, add ticket to allow this".
18 tool calls in that one turn: 1 github_list_issues, 5 repo_tree, 12 repo_read_file
of which 3 failed

❌ repo_read_file  Error: Runner /coding/read-file → 400: {"error":"ENOENT: no such file or
                   directory, stat '/Users/serge/dev/heartfull/pla…
❌ repo_read_file  Error: Runner /coding/read-file → 400: {"error":"not a regular file:
                   admin/lib/features/events/ui/pages"}
❌ repo_read_file  Error: Runner /coding/read-file → 400: {"error":"not a regular file:
                   admin/lib/app"}

Two of the three are the model passing a directory to repo_read_file. That is the tell: it had
been shown that directory as a leaf with nothing under it, and read it as a file.

Mechanism 1 — repo_tree stops at 4 levels and never says so

packages/browser-runner/src/coding/inspect.ts:129-164:

export function repoTree(workDir, relPath = ".", maxDepth = 3, maxEntries = 500) {
    const depthCap = Math.max(1, Math.min(4, maxDepth));      // :131
    
    if (entries.length >= entryCap) { truncated = true; return ; }   // :146-149
    
    if (it.isDirectory()) {
        entries.push({ path: rel, type: "dir" });
        if (depth + 1 < depthCap) queue.push({ dir: abs, depth: depth + 1 });   // :152-154
    }

Two facts compose into the bug:

  1. maxDepth is clamped to 4 and the schema never says so. The tool advertises
    maxDepth: "How many folder levels deep to walk (default 3)" (repo-local.ts:130) with no
    ceiling. A model asking for maxDepth: 10 silently gets 4. Walking from the repo root, the
    deepest path it can ever list is four segments. The file this turn was actually about is
    admin/lib/features/events/ui/pages/event_form_dialog.dartseven. It is not reachable
    from the root in any number of calls that do not already know the answer.
  2. Depth truncation sets no flag. truncated is assigned in exactly one place, the entry cap
    (:147). A directory at the depth boundary is emitted as an entry and its children are simply
    not queued — so repo-local.ts:151-157 renders it as admin/lib/features/events/ with
    res.truncated === false, i.e. without the "(truncated — narrow with path)" note.

From the model's side, "this folder is empty" and "this folder is deeper than the cap" are the same
observation. That is why it called repo_read_file on a directory, twice.

Mechanism 2 — repo_git's path is honoured by 1 of its 5 commands

repo_git advertises path: "Limit the command to one file or folder (optional)."
(repo-local.ts:214). runRepoGit resolves it (inspect.ts:93) and hands it to gitArgv, which
uses it only for diff (inspect.ts:44-63):

case "status":    return ["status", "--short", "--branch"];   // relPath dropped
case "diff":      return opts.relPath ? ["diff", "--", opts.relPath] : ["diff"];
case "diff-stat": return ["diff", "--stat"];                  // relPath dropped
case "log":       return ["log", "--oneline", "-n", String(clampN)];   // relPath dropped
case "ls-files":  return ["ls-files"];                        // relPath dropped

So repo_git {cmd:"ls-files", path:"admin/lib/features/events"} returns every tracked file in the
repository
, capped at 12 KB (repo-local.ts:38 CAPS.git) and truncated mid-list, rather than
the folder asked for. git ls-files -- <path> is the file-finder this agent has been missing the
whole time, and the parameter to reach it is already in the schema and already validated —
it is just dropped one function later.

The existing test only covers the one command that works
(packages/browser-runner/src/coding/inspect.test.ts:39-41, "only appends a path after a literal
-- separator"
, asserted for diff alone), so nothing fails.

What this costs beyond the failed calls

The chat's own account of a file is unreliable when it could not locate it. Same instance,
2026-08-12 00:02:25, the agent told the owner the fix was going into
app/lib/features/events/ui/widgets/event_form_dialog.dart; the run's completion message
(00:06:46) reports the change landed in admin/lib/features/events/ui/pages/event_form_dialog.dart.
The first path does not exist. (Inferred, not proven: I did not reproduce that the wrong path came
from the depth cap specifically — the agent had read the correct file 40 minutes earlier. I record
it as the visible consequence of an agent that navigates by guessing, not as a second mechanism.)

What to do — cheapest first

Step 1 (cloud only, ships today, no CLI release). Tell the truth in the two descriptions:
maxDepth is capped at 4 and a listing stops there; when a folder appears with no children,
call repo_tree again with path set to it rather than assuming it is empty or a file. Same for
repo_git's path: say it applies to diff only, until step 2 lands. This alone removes the
directory-as-file calls, which were 2 of the 3 failures measured.

Step 2 (runner) — honour path where git already supports it. gitArgv gains
["ls-files", "--", relPath], ["diff", "--stat", "--", relPath], ["log", "--oneline", "-n", N, "--", relPath]. Same -- discipline, same resolveInside validation, no new endpoint, no new
surface. ls-files narrowed by folder is a real file-finder.

Step 3 (runner + connector) — the actual search tool: repo_grep. git grep -n -I --untracked -e <pattern> -- <path> behind the existing /coding/git endpoint (packages/browser-runner/src/server.ts:240)
as a whitelisted command, or a sibling endpoint if the argv shape does not fit. scope: "read",
capped like the others (12 KB), fixed-position argv with the pattern after -e so it can never be
read as a flag, path after --. Declared on coder-repo and local-repo-chat by migration in the
0101/0108 shape.

Deployment constraint, stated because it decides the order. The runner ships inside the
published CLI (@proagentstore/cli, currently 0.4.48; @proagentstore/browser-runner 0.2.8),
so steps 2 and 3 reach a user only after a version bump and a CI publish — and an old runner will
400 on an unknown cmd. The new tool must therefore report "your runner is too old — run
npm i -g @proagentstore/cli" rather than surfacing a raw 400, and step 1 must not depend on either.

Alternatives considered and rejected

  • Just raise the depth cap. Rejected: 500 entries is the other cap, and a deep monorepo blows it
    before it gets deep — the owner's repos are exactly this shape. It also leaves the silent-stop
    problem, which is the part that produced wrong tool calls.
  • Let the model use the Engine to grep. That is the current workaround and it is the same
    detour [bug] A coder agent can open a GitHub issue and then never touch it — no comment, close, relabel or assign tool, so closing one ticket costs a whole Engine run #507 documents for GitHub writes: a chat question costs a coding run on the owner's
    machine, and is impossible for any agent with no runner-side session.
  • A find/ripgrep shell-out. Rejected: the connector's stated design is "no user string ever
    becomes a git token except a resolveInside-validated path after a literal --"
    (inspect.ts:11-14). git grep keeps that property; a shell does not.
  • Making repo_tree return truncatedByDepth. Worth doing as part of step 2, but it is not a
    substitute for step 1: the flag has to be rendered into the tool result the model reads, and the
    description fix ships without a runner release.

Acceptance criteria

  • repo_git {cmd:"ls-files", path:"admin/lib/features/events"} returns only that subtree.
  • inspect.test.ts asserts the -- path for ls-files, diff-stat and log, not just diff.
  • repo_tree on a folder at the depth cap makes the stop visible in the tool result — the model
    can tell "empty" from "deeper than I showed you".
  • repo_grep finds a symbol in a 7-segment path in ONE call, on the Heartfull checkout, and the
    answer cites the real path.
  • Replaying the 2026-08-11 22:28 question produces zero repo_read_file calls whose argument
    is a directory.
  • Against a runner older than the release, the new tool reports the version problem in words;
    it does not surface Runner /coding/git → 400.

Regression risk

  • A new git command is a new argv. The whole safety argument of this connector is the fixed-argv
    whitelist; git grep takes a user-supplied pattern, which nothing here has done before. It must
    go after -e at a fixed position, never concatenated, and the existing "throws on an unknown
    command" test must be joined by one asserting a pattern that looks like a flag stays a pattern.
  • Output volume. git grep on a broad pattern can return megabytes; the 12 KB cap must be
    applied to it and reported as truncated, or one call eats the context window the tool exists to
    save.
  • Old runners. Covered by the last criterion; without it this fix converts "no search tool" into
    "search tool that errors", which is worse for anyone who has not upgraded.

Activity

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

    bugSomething isn't workingconnectorsConnector + tool framework

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions