Repository navigation
[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
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:
maxDepthis 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 formaxDepth: 10silently 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.dart— seven. It is not reachable
from the root in any number of calls that do not already know the answer.- Depth truncation sets no flag.
truncatedis 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 — sorepo-local.ts:151-157renders it asadmin/lib/features/events/with
res.truncated === false, i.e. without the"(truncated — narrow withpath)"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 droppedSo 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/ripgrepshell-out. Rejected: the connector's stated design is "no user string ever
becomes a git token except aresolveInside-validated path after a literal--"
(inspect.ts:11-14).git grepkeeps that property; a shell does not. - Making
repo_treereturntruncatedByDepth. 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.tsasserts the--path forls-files,diff-statandlog, not justdiff. -
repo_treeon 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_grepfinds 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_filecalls whose argument
is a directory. - Against a runner older than the release, the new tool reports the version problem in words;
it does not surfaceRunner /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 greptakes a user-supplied pattern, which nothing here has done before. It must
go after-eat 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 grepon 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
feat(coding): a Repo Coder can find a file — and repo_tree and repo_g…
test(connectors): the repo-local tool count follows the two search to…
serge-ivo commentedon Aug 12, 2026
Fixed in 7ea6359, with a fix-forward in f4aa59c. Both on
main; CI green on f4aa59c; API Worker deployed; CLI published.All three causes addressed.
repo_git'spathnow reaches all five commands, each after git's literal--separator, so the "no user string becomes a git token except a validated path after--" property is unchanged.statusis included too — the description can then say "applies to every command" with no caveat, and a caveat is something a model has to reason about and can get wrong.repo_tree's depth stop is visible. Each directory whose contents were not listed is marked, and the listing says it stopped. Worded "contents not listed" rather than "has more inside" on purpose: confirming the latter costs a readdir per boundary directory and the model's next move — call again withpath— is the same either way.maxDepth's ceiling of 4 is now stated in the schema.repo_find+repo_grep, two tools over one runner endpointPOST /coding/search.Shape of the search tool, and why. Two tools rather than one with a
mode: that argument would make "am I looking for a name or a usage?" a parameter the model must get right on the exact turn where it is already lost. One runner endpoint keeps a single version skew to reason about.Bounding was the real decision. A byte cap alone moves the problem into
TOOL_RESULT_MAX_CHARS— how #503 happened. Every bound is COUNTED: 50 results total, git's own--max-count=5per file so a minified bundle cannot spend the budget, 160 chars per line, 4MB stdout ceiling because git's output is read into memory before any of those apply. Worst case ~11KB. A cut list reportsshowing 50 of 812, which is what tells the model to narrow rather than conclude.Fixed strings, not regexes (
-F) — a deliberate deviation from the argv sketched above.foo(is the obvious thing to search for in code and an invalid regex, and no pattern the model invents can become a pathological match on the owner's own machine. Inpathmode the pattern never becomes a git token at all: the filter runs in JS overls-files --cached --others --exclude-standard, so an uncommitted file is found and:(exclude)srcmatches nothing rather than doing something.THE OWNER MUST UPGRADE HIS RUNNER. His Mac mini is on 0.4.45.
npm i -g @proagentstore/cli(now 0.4.49, published by CI on this commit — verified withnpm view) and restartpags up. Below that,repo_find/repo_grep404 and report "this machine's runner is too old — it needs CLI 0.4.49 or newer" rather than a raw 404, andrepo_gitwarns that thepathfilter was ignored and the output covers the whole repository. Both description fixes work on any runner today.Verified. 27 runner tests including a file seven segments down that no
repo_treecall could reach, the:(exclude)pathspec case, an uncommitted file, and the counted-truncation report; 46 connector tests including the old-runner 404 and the "does not blame the CLI for a 500" half. Migration0121_repo_search_tools.sqlconfirmed applied to remote D1 in the deploy log (✅), declaring both tools oncoder-repoandlocal-repo-chat;coder/coder-leadexcluded because neither declares a repo_* tool.Not verified. No live agent turn — that needs the owner's machine on 0.4.49 and a session. The declaration is confirmed applied, not exercised.
Note on f4aa59c. 7ea6359 went out with
registry.test.tsstill asserting four repo-local tools, somainwas briefly red on a count that same commit made wrong: several sessions share this checkout and branch, and a concurrent session pushed before the amend carrying that fix could be. Nothing lost; fixed forward within minutes.feat(security): every registry tool declares whether its result is te…
fix(queue): a runner that misses a ping is a wait, not a refusal - qu…