DEV Community

Cover image for Help Your Coding Assistant Find the Right Code in Large Repos
Saurabh "Rob" Dahal
Saurabh "Rob" Dahal

Posted on Edited on

Help Your Coding Assistant Find the Right Code in Large Repos

Get the starter: sdahal1/repo-search-workshop on GitHub. Rehearsed with Node.js 24.10.0, npm 11.6.0, ripgrep 15.2.0, and ast-grep 0.51.0.

Author: Saurabh Rob Dahal, Developer Advocate, AWS

Description: Find and fix a bug in an unfamiliar repository by having your coding assistant search instead of read: map the repository, search by text and by code shape, follow symbols, then fix with evidence.

Level: Beginner · Time: 45 minutes of hands-on work plus 15 minutes of Q&A, after setup

A coding assistant can suggest a plausible fix before it understands a repository. On a large codebase, it often gets there by reading file after file until something looks wrong. That fills its context with code that doesn't matter, and you can't see how it reached its answer.

This workshop practices the opposite habit: map first, read less. You narrow the problem to one package, use targeted searches to find the few lines that matter, and give the assistant only that evidence before it changes anything.

You'll investigate one prepared bug. A client configured with maxRetries: 0 should give up after one failed request, but it sends three. The bug is small on purpose. The method is what carries over to your own code.

You can use Kiro, Claude Code, Codex, or another assistant that can read files, run shell commands, and edit code. No AWS account is required.

How this workshop works

There are 9 tasks across 6 steps. Each one is a Task heading with a short prompt or command, followed by You should see, describing a correct result. Everything else explains why the step matters, and you can read it later.

You won't type search commands yourself. Each prompt tells the assistant what to find, where, and which tool to use, and it writes and runs the command. The ground rules you set at the start make it show you each command and its raw output.

After each assistant prompt, an Expected commands section shows what the assistant should have run, what each command does for you, and why it matters. During the live session, glance at it to confirm the assistant did the right kind of work: searched instead of reading, used the tool you asked for, and stayed in scope. Read the full explanations afterward. If the commands don't match, that's a sign the prompt didn't steer the assistant, and you can ask it to redo the step.

Time Step Tasks
0–3 min Start: set the ground rules 1
3–6 min 1. Reproduce the bug 1
6–11 min 2. Map the repository 1
11–21 min 3. Search by text, then by shape 2
21–26 min 4. Follow the symbols 1
26–35 min 5. Write the evidence and get an explanation 2
35–43 min 6. Make the fix and verify it 1
43–45 min Wrap up 0
45–60 min Q&A

Before the session

Do this ahead of time. It isn't part of the 45 minutes.

1. Install the tools: Git, Node.js 24 with npm, ripgrep, an editor with TypeScript Go to Definition, and a coding assistant you've signed in to. On a Mac with Homebrew, run brew install ripgrep.

2. Clone the starter and install its dependencies:

git clone https://github.com/sdahal1/repo-search-workshop.git
cd repo-search-workshop
git switch -c workshop/my-investigation
npm ci
npx --no-install ast-grep --version
git status --short
Enter fullscreen mode Exit fullscreen mode

You should see: ast-grep reports 0.51.0, and git status --short prints nothing. npm ci installs the exact dependency versions in the lockfile, including ast-grep, so everyone has the same tools. The empty status is your starting baseline: in step 6, anything that shows up in git diff came from your work.

3. Open the whole repo-search-workshop folder in your editor, not a single file, so TypeScript navigation works.

Start: set the ground rules

Time: 0–3 minutes

Task 0: Give the assistant its ground rules

Start a conversation with your assistant in the repository and paste:

For this investigation, follow these rules until I say otherwise:
- Ignore the workshop/ folder unless I name a file in it.
- You can read docs, READMEs, and package.json files. To find code,
  search instead of opening source or test files. Use rg for text and
  ast-grep for code shape (npx --no-install ast-grep). Show me each
  command and its raw output.
- Only edit files I name.
- Report what you find. Don't suggest causes or fixes until I ask.
Enter fullscreen mode Exit fullscreen mode

You should see: The assistant acknowledges the rules. If you start a new conversation later, paste them again.

Each rule protects part of the method:

  • Ignore workshop/. It holds the exercise material. Assistants treat everything in scope as evidence, including hints and stale notes. Real repositories have the same problem.
  • Read the small files, search the code. Docs, READMEs, and manifests are short and describe what the code should do, so reading them is cheap. Source and test files are where the volume is, so those get searched. Showing each command keeps the assistant's context small and its work visible. Without it, the assistant may read files and summarize, and you can't tell what it skipped.
  • Edit only what I name, and no causes yet. Assistants tend to jump ahead to a fix. Keeping investigation and editing separate is what lets you check the reasoning before code changes.

1 Reproduce the bug

Time: 3–6 minutes

Why: A reproduction turns a vague report into a measured gap between expected and actual behavior. It also becomes your first check after the fix.

The alternative: Paste the bug report and say "fix it." But then nobody has defined what "correct" means, and you can't show that a change fixed anything.

Task 1: Run the reported scenario and the tests

The bug report in workshop/BUG-REPORT.md says: "I set maxRetries to zero, but the client still retries." Reproduce it the two ways you would on a real project, by running the scenario and running the test suite:

npm run demo
npm test
Enter fullscreen mode Exit fullscreen mode

You should see:

  • npm run demo prints Transport calls: 3. The demo makes one request with maxRetries: 0 and a transport that always fails. A transport is the function that sends each request; here it's a fake that fails and counts its calls. The demo doesn't say whether 3 is right. That's for you to decide against the docs in step 2.
  • npm test reports 27 passing and 1 failing: maxRetries: 0 makes one attempt, ending in 3 !== 1. The test expected one call and got three.

In a real project, you'd usually write that failing test yourself from the bug report. It's already in the starter to save time.

A retry is an extra attempt after the first. Zero retries should mean one call total.

Takeaway: "Retries when it shouldn't" is now "1 call expected, 3 observed," with two commands that show it. Step 6 reruns the same two commands, so before and after compare directly. Note that this is an observation, not a cause. The loop might be off by one, the setting might be dropped, or a default might be applied. Keep all three open.

2 Map the repository

Time: 6–11 minutes

Why: Before reading any code, figure out which part of the repository owns the behavior and what correct behavior is. Paths, manifests, and docs are small, so this is cheap.

The alternative: Let the assistant explore however it likes. On a large repository, that means hundreds of files in its context, and you can't check how it chose where to look.

Task 2: Ask for a map

Use git ls-files to list the files, skipping workshop/. Then map the
repository from that list, the README, package.json files, and docs.
Don't read source or test files yet.

Bug: with maxRetries: 0, the client makes three calls instead of one.
Which package most likely owns this, and why? Why not the others?
Enter fullscreen mode Exit fullscreen mode

You should see:

  • The owner: packages/client. Its package.json says it handles "transport retry behavior," and docs/client-options.md documents maxRetries as a client option.
  • Ruled out, each with a reason:
    • packages/jobs also has a maxRetries option, but docs/jobs.md says job retries are separate from client retries. The bug is about client calls.
    • packages/config only reads environment variables, and packages/telemetry only formats and logs.
    • The apps call the client; they don't implement retries.
  • The contract, from docs/client-options.md: maxRetries: 0 means one call, 1 means two calls, and leaving it out means the default of two retries, or three calls.

Expected commands:

git ls-files -- . ':!workshop'
Enter fullscreen mode Exit fullscreen mode

Then file reads of README.md, the package.json files, and the files in docs/, and no reads of .ts files.

Command or action What it does for you Why it matters here
git ls-files -- . ':!workshop' Lists every file Git tracks, about 60 here, without opening any of them. ':!workshop' excludes the exercise folder. The file list is the cheapest map there is: a few kilobytes, even for a large repository. Folder and file names alone often tell you where a behavior lives.
Reading the README and package.json files Shows what each package says it's responsible for. These files are short and written by people to explain intent. One sentence, "transport retry behavior," points at the owner faster than reading any code.
Reading the files in docs/ Gives the documented contract for maxRetries, and explains how job retries differ from client retries. You can't judge any fix without knowing what correct means. The docs also rule out packages/jobs, the one package a name search alone would confuse with the owner.

If the assistant used rg --files instead of git ls-files, that's fine. It does the same job but skips hidden files, so it lists a few fewer. If it opened .ts files, it read more than it needed to. Remind it of the ground rules.

Takeaway: You narrowed about 60 files to one package, without reading any code, and you know what correct looks like. That matters more here than it might seem: maxRetries also appears in the jobs package, the config package, and both apps. Without the map, any search for it would mix all of them together.

How far we've narrowed: 61 files → 11 source files in one package. Three other packages and three apps are ruled out, and the assistant hasn't opened a single .ts file.

One clue stands out: zero produces the same three calls as leaving the option out. That hints that something treats zero as "not set." It's still a guess. The searches will check it.

3 Search by text, then by shape

Time: 11–21 minutes

Why: Text search finds every place a name appears. Structural search finds every place code has a certain shape, even when names, spacing, or line breaks differ. Together they find the few lines that matter without reading whole files.

The alternative: Ask "where is maxRetries handled?" and accept the answer. The assistant may read the files it guesses are relevant and stop at the first plausible match. You can't see what it skipped, and asking again may give a different answer.

Task 3.1: Search for the setting by name

Search for the most specific name in the bug, the setting that didn't take effect, inside the package from step 2.

Use rg to search packages/client/src for maxRetries, with a few lines
of context.
Enter fullscreen mode Exit fullscreen mode

You should see: 9 matching lines across 5 files. Most just declare, validate, or pass the value along. One line in retry/policy.ts stands out:

const maxRetries = options.maxRetries || DEFAULT_MAX_RETRIES;
Enter fullscreen mode Exit fullscreen mode

That's where a fallback can replace the setting, which fits the "zero acts like not set" clue from step 2.

Expected command:

rg -n -C 3 'maxRetries' packages/client/src
Enter fullscreen mode Exit fullscreen mode
Part What it does for you
rg 'maxRetries' Finds every line containing the name, across all files under the path.
packages/client/src Limits the search to the source of the package from step 2. You already saw the failing test in step 1.
-n Adds line numbers, so every result is a location you can cite later.
-C 3 Shows 3 lines above and below each match, so you can see what the code does with the value without opening the file.

Why this matters: The scope is doing most of the work. The same search across the whole repository returns 55 lines in 20 files, including the jobs package's own maxRetries, the config loader, both apps, and the docs. Scoped to the package the map picked, it returns 9. The assistant reads only those lines and a little context, not 11 files in full. You see every match, so nothing is skipped silently, and anyone can rerun the command and get the same result.

If the assistant searched the whole repository, or opened files instead of searching, the scope is wrong. Ask it to rerun the search on packages/client/src.

Show it on screen (optional, about 1 minute). Run these yourself to make the effect of the map visible. The only difference between the last two commands is the scope from step 2:

git ls-files -- . ':!workshop' | wc -l              # 61: every file in the project
rg -l maxRetries --glob '!workshop/**' . | wc -l      # 20: files that mention maxRetries, whole repo
rg -l maxRetries packages/client/src | wc -l        # 5:  same search, after the map
Enter fullscreen mode Exit fullscreen mode

rg -l lists only the names of matching files, and wc -l counts them.

How far we've narrowed: 20 files that mention maxRetries → 5, because the search stayed inside the package the map picked. That's 9 lines for the assistant to read instead of 55.

Task 3.2: Find every expression with the same shape

Before blaming that line, check whether the same pattern appears anywhere else in the package. Some copies may be intentional.

retry/policy.ts line 10 has: options.maxRetries || DEFAULT_MAX_RETRIES

Find every expression in packages/client/src with that same
value || fallback shape. Use rg for a text search and ast-grep for a
structural search, and show both. Then keep only real fallbacks, not
boolean conditions, and check docs/client-options.md to see whether
each one is intended.
Enter fullscreen mode Exit fullscreen mode

You should see:

  • Text search: 5 lines, but not 5 expressions. One result is only || 'workshop-client'; because its left side is on the line above. Text search finds characters, not shapes.
  • Structural search: 5 whole expressions, including that two-line one.
  • Fallback candidates for this repository: 3 matches:
    • request.ts:6-7: options.requestLabel || 'workshop-client'
    • headers.ts:6: options.userAgent || DEFAULT_USER_AGENT
    • retry/policy.ts:10: options.maxRetries || DEFAULT_MAX_RETRIES
  • Intent: The docs say an empty label and an empty user agent should both use their defaults. That's exactly what || does, so those two are intended. The docs say zero retries is valid, so for maxRetries, || is wrong. Same shape, different intent.

Expected commands: three searches, in this order. The exact ast-grep syntax will vary between assistants.

# 1. Text search for the operator
rg -n -F '||' packages/client/src

# 2. Structural search for the shape
npx --no-install ast-grep run --lang ts \
  --pattern '$VALUE || $FALLBACK' packages/client/src

# 3. Structural search, fallback candidates for this repository
npx --no-install ast-grep scan --inline-rules '
id: or-fallback
language: TypeScript
rule:
  pattern: $VALUE || $FALLBACK
constraints:
  VALUE:
    kind: member_expression
  FALLBACK:
    any:
      - kind: identifier
      - kind: string
      - kind: number
' packages/client/src
Enter fullscreen mode Exit fullscreen mode
Command What it does for you Why it matters here
1. `rg -F '\ \ '`
2. ast-grep run --pattern Parses the TypeScript and finds every expression shaped like `something \ \
3. {% raw %}ast-grep scan --inline-rules Same pattern, plus constraints: the left side must read a property (member_expression), and the right side must be a plain name, string, or number. It filters out boolean conditions like `a === true \

Why this matters: One suspicious line is a lead. Checking for others with the same shape tells you whether the bug is one line or a pattern repeated across the package, and whether a "fix everything that looks like this" change would break working code. Here it would: two of the three fallbacks are correct. The structural search found all three without the assistant reading any file in full.

If the assistant used only {% raw %}rg, or wrote a regular expression to match || with something on each side, it didn't do a structural search. Ask it to use ast-grep. Regular expressions can't reliably match code that spans lines or nests.

Takeaway: Five || expressions became three fallbacks, then one suspect. The boolean conditions dropped out by shape, and the two siblings dropped out by intent. That's also why replacing every || in the package would break working code: a structural match is a lead to check, not a diagnosis.

How far we've narrowed: 5 files → 3 files with a fallback of this shape → 1 suspect line, retry/policy.ts:10.

4 Follow the symbols

Time: 21–26 minutes

Why: A suspicious line only matters if the failing test actually reaches it, and with the value you expect. Your TypeScript editor's Go to Definition resolves which declaration a name refers to, even across renamed imports. Text search finds matching names; editor navigation resolves their identity. Reading the relevant code then shows how the value flows.

The alternative: Let the assistant trace imports by reading files. It works on small repositories but gets unreliable with re-exports and renamed imports, and it costs context. Here, the path crosses three files and two folders.

Task 4: Trace the path from the test to the suspect

In your editor:

  1. Open packages/client/test/client.test.ts, find the test maxRetries: 0 makes one attempt, and use Go to Definition on createClient.
  2. In client.ts, find the line that calls resolveRetryPolicy. Note that it passes options.maxRetries along unchanged. Use Go to Definition on resolveRetryPolicy.
  3. Back in client.ts, use Go to Definition on withRetry to see where the policy is used.

You should see:

  • createClient passes options.maxRetries straight into resolveRetryPolicy (client.ts:9).
  • resolveRetryPolicy is in retry/policy.ts, and line 10 is the suspect from step 3.
  • withRetry in retry/run.ts keeps retrying until it has used policy.maxRetries retries (run.ts:14).

During setup, the test calls createClient, which calls resolveRetryPolicy and stores its returned policy. During the request, client.request passes that policy to withRetry. Zero reaches retry/policy.ts untouched, and the loop uses the returned policy. The policy function does not call the retry loop directly.

Takeaway: The suspect is on the path the failing test takes, and nothing changed the value before it got there. That rules out "the setting was dropped" from step 1. You also found where the loop reads the result, which rules out "the loop is off by one" once you see it uses the policy's number as is.

How far we've narrowed: The failing test's path runs through 3 files: client.ts, retry/policy.ts, and retry/run.ts. Those are the only source files the explanation needs.

5 Write the evidence and get an explanation

Time: 26–35 minutes

Why: A short evidence note gives the assistant exactly what it needs and nothing else. Asking for an explanation before any edit lets you check the reasoning while changing your mind is still cheap.

The alternative: Keep everything in the chat and say "fix it." Long chats get summarized and early evidence drops out. And a diff on its own doesn't show whether the reasoning was sound: a change to the loop and a change to the default both look plausible.

Task 5.1: Have the assistant write the evidence

Fill in the Observed behavior, Repository area, and Relevant code
sections of workshop/INVESTIGATION.md from what we found. Include the
call path I followed: test → createClient → resolveRetryPolicy →
withRetry. Keep it to facts; leave the hypothesis sections empty.
Enter fullscreen mode Exit fullscreen mode

Then review it:

git diff workshop/INVESTIGATION.md
Enter fullscreen mode Exit fullscreen mode

You should see: Only the note changed. The Relevant code section has about five rows on the path from the test to the loop: the test (client.test.ts:31), client.ts:9, retry/policy.ts:10, and retry/run.ts:14, plus the option type if it's included. Each has a path and line number you saw in a search result. Delete any row that's off the path, and any guess about the cause. Those belong in the next task.

Expected commands: one file edit, to workshop/INVESTIGATION.md. No searches and no other edits. The assistant should write the note from the search output already in the conversation.

Action What it does for you Why it matters here
Editing only workshop/INVESTIGATION.md Turns the conversation into a short, structured file. Chat history gets long and may be summarized. A file keeps the evidence intact, and you or a teammate can hand it to any assistant later.
git diff workshop/INVESTIGATION.md (you run it) Shows exactly what the assistant added. It's your check that every row is a fact you saw in a search, not a guess.

If the assistant ran new searches or opened source files to write the note, it's re-collecting evidence it already has, which costs context. If it edited anything besides the note, undo that change.

Task 5.2: Ask for an explanation before any edit

Read workshop/INVESTIGATION.md and only the lines it points to.
Explain the bug: the cause, the evidence with line numbers, the
smallest fix, and which tests would prove it. Don't change anything yet.
Enter fullscreen mode Exit fullscreen mode

Optional: do this in a fresh conversation (paste the ground rules first). If the note is good, a fresh assistant can explain the bug from it alone.

You should see: An explanation like this: zero is falsy, so 0 || DEFAULT_MAX_RETRIES picks the default of 2. The loop allows two retries, so the transport is called three times. The fix is ??, which falls back only when the value is undefined or null, so zero stays zero. Nullish coalescing in TypeScript

| Value of x | x \|\| 2 | x ?? 2 |
| --- | --- | --- |
| 0 | 2 (wrong here) | 0 |
| undefined (option left out) | 2 | 2 |
| 1 | 1 | 1 |

Before accepting it, check that it connects all three pieces using evidence from your note: zero arrives unchanged (client.ts:9), || replaces it with 2 (retry/policy.ts:10), and two retries means three calls (retry/run.ts:14). An answer that only says "use ??" hasn't explained the whole symptom.

A good explanation may also point out why the existing tests didn't catch this: retry-policy.test.ts checks an omitted value and a positive value, but not zero. That's a realistic way bugs at a boundary slip through.

Expected commands: a read of workshop/INVESTIGATION.md, then targeted reads of the few lines it cites, mainly around retry/policy.ts:10, client.ts:9, and retry/run.ts:14. No edits.

Action What it does for you Why it matters here
Reading the note first Gives the assistant the evidence and the scope in one short file. This is the payoff of the whole investigation: the assistant starts from a handful of cited lines instead of about 60 files.
Reading only the cited lines Lets the assistant confirm each claim in the source. It can verify the evidence without spending context on unrelated code.
No edits Keeps this step to reasoning only. You can reject a wrong explanation in seconds. Rejecting a wrong edit means undoing files.

If the assistant read whole files or searched the repository again, your note may be missing something. Check what it went looking for, and add that to the note.

Takeaway: You have an explanation that accounts for the exact numbers you observed, backed by lines you can point to, before any code changed.

How far we've narrowed: The assistant explained the bug from a note citing 4 relevant locations and their nearby context, rather than 61 files. That's the payoff of the whole method.

6 Make the fix and verify it

Time: 35–43 minutes

Why: A fix has to correct the bug and keep everything else working. Tests, a demo run, and the diff each check something different.

The alternative: Accept "fixed, tests pass." But the assistant may have run the wrong command, changed the test to match the bug, or replaced every || in the package.

Task 6: Make the fix and check it yourself

Edit only packages/client/src/retry/policy.ts to make the smallest
fix for this bug. Keep the public API, default behavior, and existing
test assertions. Then run npm test.
Enter fullscreen mode Exit fullscreen mode

Then run the checks yourself rather than trusting a summary:

npm test
npm run demo
git diff
Enter fullscreen mode Exit fullscreen mode

You should see:

  • npm test: all 28 tests pass, including the reproduction and the test that leaving the option out still gives three calls.
  • npm run demo: Transport calls: 1. The fake transport still reports a rejected request; that is intentional. The fix changes the number of calls, not whether the fake succeeds.
  • git diff: one line changed in retry/policy.ts, from || to ??, plus your note. For extra coverage afterward, you can separately authorize a new zero case in packages/client/test/retry-policy.test.ts. That would make 29 tests; the main task keeps the 28 existing tests unchanged. Nothing else should change.
- const maxRetries = options.maxRetries || DEFAULT_MAX_RETRIES;
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
Enter fullscreen mode Exit fullscreen mode

Expected commands: one edit to packages/client/src/retry/policy.ts, then npm test.

Command What it does for you Why it matters here
Edit to retry/policy.ts Changes the one line the evidence points to. A small, targeted edit is easy to review. An edit to request.ts, headers.ts, or an existing assertion means the assistant went beyond the evidence.
npm test (assistant, then you) Runs all 28 tests across the four packages. The reproduction checks the bug is fixed. The others check that nothing else broke, like the default still giving three calls, or the label and user-agent fallbacks still working.
npm run demo (you) Reruns the original scenario from step 1. It confirms the symptom from the bug report is gone, in the same terms the report used.
git diff (you) Shows every line that changed since your clean start. It's the only check that sees the whole change. Tests can pass while an unrelated file was edited.

You run the checks yourself because a summary like "all tests pass" is easy to write without checking. The assistant may have run a different command, or misread the output.

If the diff touches request.ts, headers.ts, or an existing test assertion, ask why. Those fallbacks are intended, and changing a test's expected count hides the bug instead of fixing it.

Takeaway: A one-operator fix, confirmed three ways: the tests check behavior, the demo checks the original symptom, and the diff checks that nothing else changed. It also fixes the worker app, which sets maxRetries: 0: when each attempt throws a retryable error, the client now makes one request per job run instead of three.

How far we've narrowed: 1 line in 1 file changed, out of 61 files.

Wrap up

Time: 43–45 minutes

Here's how far each step narrowed the search:

Step What was left to look at How
Start 61 files
2 Map 1 package, 11 source files File list, manifests, docs
3.1 Search by name 9 lines in 5 files rg, scoped to the package
3.2 Search by shape 5 expressions → 3 fallbacks → 1 suspect ast-grep, then the docs
4 Follow Confirmed on the failing test's path Go to Definition
5 Explain One explanation, 4 cited locations Evidence note
6 Fix 1 changed line Tests, demo, git diff

Each step answered one question: what's wrong, where to look, where the name and shape appear, whether the test reaches the suspect, whether we understand it, and whether the fix is complete. The assistant read a handful of lines, not the whole repository, and you could check every step. On a repository with thousands of files, the same steps narrow the search the same way, and that's the difference between an assistant that finds the bug and one that gets lost.

Use this on your own codebase

Install ripgrep and ast-grep where your assistant runs commands. On a Mac: brew install ripgrep ast-grep. Without ast-grep, the assistant may quietly fall back to a text search; checking the command it shows you catches that.

Then paste these in order, filling in the angle brackets.

Ground rules:

For this investigation, follow these rules until I say otherwise:
- You can read docs, READMEs, and package manifests. To find code,
  search instead of opening source or test files. Use rg for text and
  ast-grep for code shape. Show me each command and its raw output.
- Only edit files I name.
- Report what you find. Don't suggest causes or fixes until I ask.
Enter fullscreen mode Exit fullscreen mode

Map:

Use git ls-files to list the files. Then map the repository from that
list, the README, package manifests, and docs. Don't read source or
test files yet.

Bug: <what happens, with the input and the result>.
Which package most likely owns this, and why? Why not the others?
Enter fullscreen mode Exit fullscreen mode

Search by name, then by shape:

Use rg to search <package> for <setting, error message, or log text>,
with a few lines of context.
Enter fullscreen mode Exit fullscreen mode
<file> line <n> has: <suspicious expression>
Find every expression in <package> with that same shape. Use ast-grep
for a structural search. Check whether each one is intended.
Enter fullscreen mode Exit fullscreen mode

Follow the symbols in your editor with Go to Definition, from the failing test or entry point to the suspect.

Explain, then fix:

Explain the bug from what we found: the cause, the evidence with line
numbers, the smallest fix, and which tests would prove it.
Don't change anything yet.
Enter fullscreen mode Exit fullscreen mode
You can edit code now. Make the smallest fix. Keep <what must not
change> and the existing test assertions.
Enter fullscreen mode Exit fullscreen mode

Then run your tests and read git diff yourself before you commit.

If something goes wrong

Symptom What to do
The assistant answers without showing a command Ask it to rerun the step as a search and show the command and raw output.
The assistant used a text search when you asked for structure Ask it to use ast-grep with npx --no-install ast-grep.
ast-grep is missing or reports a different version From the repository root, rerun npm ci.
Go to Definition doesn't work Open the whole repository folder, not a single file, and wait for TypeScript to load.
The reproduction already passes You may have an earlier edit. Check git diff, or clone a fresh copy.
The assistant edits code before step 6 Run git diff, undo the change with git checkout -- <file>, and paste the ground rules again.
The fix passes the reproduction but breaks another test Compare the failing test with the docs, and revisit the explanation instead of changing the test.

No cloud resources are created, so there's nothing to clean up.

Top comments (6)

Collapse
 
reidmarlow profile image
Reid Marlow •

Separating the search step from the edit prompt is the single biggest reliability boost when pointing an agent at a repo it has not indexed yet.If you let a tool like Claude Code or Codex do free-form string searches across the whole workspace, it almost always gloms onto variable names inside fixture mocks or test assertions before it ever finds the actual transport layer. Forcing it to run structural queries with ast-grep or grep with path boundaries first anchors the context on the call-site signature. Once the model has the exact AST node and the surrounding ten lines pinned in its evidence buffer, it stops trying to solve the problem by hallucinating fallback options on the wrapper.

Collapse
 
hacksore profile image
Sean Boult •

rg (mentioned here) and fzf are two pieces of fantastic software!

Collapse
 
skepagent profile image
SkepAgent •

The part I'll actually borrow isn't the tool list, it's step 6. Handing the assistant INVESTIGATION.md in read-only mode before asking for an explanation is the cheapest possible context pack — and it leaves a reviewable artifact instead of a chat transcript. One addition that worked for me on a similar || defaulting bug: save the ast-grep rule from step 4 and keep running it. $VALUE || $FALLBACK against option-like property reads is exactly the kind of pattern that regresses quietly, and catching the next one at review time costs nothing compared to debugging it at runtime.

Collapse
 
makayis_90942586425f8f6bb profile image
makayis •

"A coding assistant can suggest a plausible fix before it understands a repository" sums up most bad agent sessions. A side benefit of your search-first order (rg → ast-grep → symbol navigation) is cost: targeted matches are much smaller than having the assistant read whole files, and an evidence note is a compact thing to carry forward when a long session gets summarized. Do you recommend feeding the evidence note back to the assistant at the fix step, or keeping it for the human reviewer only?

Collapse
 
lin7c profile image
xcl •

The step I keep coming back to is step 4's counterexample problem: the broad $VALUE || $FALLBACK pattern also matches isAdmin || isOwner, where the OR is intentional, and the narrower pattern for maxRetries misses a computed property read or a value first assigned to a local variable. That trade-off feels like the real skill here, not just running ast-grep. In a large repo, how do you decide when to stop narrowing the pattern and start relying on symbol references instead? It seems like structural search is best for finding candidate shapes, while LSP references are what actually confirm the data flow for a specific option.

Collapse
 
bhkbdbhatt profile image
Bhargav Bhatt •

Can you please take a look at github.com/bhkbdbhatt/mono-repo-pr...

This tool helps in "Layered, phased prompt generator for monorepos — task-driven file selection, auto-generated architecture maps, token-budget enforcement, and a state loop for progressive LLM reading."