Grep
# grade this stage (from your course directory)
npx theinference test --stage grep --watchGlob finds files by name; Grep finds them by content. Hand it a regex and a starting path and it scans every line of every file underneath, returning each hit as file:line:text — the same shape ripgrep prints, and the same shape the model has seen a thousand times in training, so it parses the results without being taught. This is the last read tool. After this, the model can locate the world before it changes anything.
grep("foo", ".") over a.txt = "foo\nbar\nfoobar" b.txt = "baz"
a.txt:1:foo
a.txt:3:foobar (line 2 "bar" doesn't match; b.txt has no "foo")
What to build
Implement Tools.grep
Implement Tools.grep(pattern, path) in src/tools.ts:
- Resolve
paththrough the sandbox; error if it can't be resolved. - Compile
patternas aRegExp(treat it as a real regex, not a literal). - If
pathis a directory, walk it recursively, visiting entries in sorted order at each level; if it's a file, scan just that file. - For each file, split on
\nand test each line; on a match push${rel}:${lineNo}:${line}whererelis the path relative to the sandbox root andlineNois 1-indexed. - Return the matches joined by newlines, in deterministic order.
The program.ts grep subcommand builds a sandbox from case.files and prints tools.grep(case.pattern, case.path).
grep(pattern: string, path: string): ToolOutput {
// resolve path -> error if it can't resolve
// compile pattern as a RegExp
// walk dirs in SORTED order (or scan a single file); 1-indexed line numbers
// push "${rel}:${lineNo}:${line}" per match; join with "\n"
}
Two non-determinism traps the grader catches. First, readdirSync returns entries in filesystem order — sort the names at each directory level so the file order is stable across machines. Second, lines are 1-indexed: the match on the first line is :1:, not :0:. Within a file, emit matches in file order (top to bottom). Get either wrong and the byte diff fails even when the set of matches is right.
Compile with new RegExp(pattern) and test each line with re.test(line). That means foo matches the substring foo anywhere on the line (so foobar matches), and metacharacters like . or ^ behave as regex — that's intentional, it mirrors ripgrep's default. The fixture pattern foo hits foo on line 1 and foobar on line 3 of a.txt, and nothing in b.txt.
Process mode. The harness runs npx tsx program.ts grep fixtures/case.json and compares the printed JSON to fixtures/expected.json. The expected {"content":"a.txt:1:foo\na.txt:3:foobar"} pins format, line numbers, and order all at once — relative paths (not the temp prefix), 1-indexed lines, colons as separators, and the matched line text verbatim.