Skip to content

feat(code-nav): file-exploration bundle — list_files + read_file + grep_file - #20

Merged
jlitola merged 1 commit into
mainfrom
feat/code-nav-file-exploration
Apr 21, 2026
Merged

feat(code-nav): file-exploration bundle — list_files + read_file + grep_file#20
jlitola merged 1 commit into
mainfrom
feat/code-nav-file-exploration

Conversation

@jlitola

@jlitola jlitola commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Ships three capability-gated tools that complete the "which files does this dependency ship? → read one → search inside it" chain for agents investigating third-party source. Each is available as both a CLI subcommand and an MCP tool:

CLI MCP Purpose
githits code files list_files Enumerate files in an indexed dependency
githits code read read_file Read a file (full or --lines N-M)
githits code grep grep_file Case-insensitive substring search within one file

All three sit behind the existing code_navigation capability gate, dual-addressed (registry+package_name XOR repo_url+git_ref), and share the indexing lifecycle used by the shipped search_symbols tool.

The workflow

code files npm:express lib                          # discover paths
code read  npm:express lib/express.js --lines 1-40  # read content
code grep  npm:express exports lib/express.js       # find a string

The path field is shared verbatim across the three envelopes (list_files.files[].pathread_file({path}) / grep_file({path})) so chaining has no rename friction.

Design

  • Dual addressing via the shipped codeTargetSchema + resolveCodeTarget. XOR enforced; the INVALID_ARGUMENT envelope carries retryable: false.
  • Indexing lifecycle → typed error. GraphQL PACKAGE_INDEXING errors and data-path indexingStatus: \"INDEXING\" sentinels both collapse to {code: \"INDEXING\", retryable: true, details: {indexingRef, availableVersions}}. Default wait 20 s (DEFAULT_WAIT_TIMEOUT_MS), overridable by --wait / wait_timeout_ms.
  • Data-first envelopes. No indexing metadata on the success path. filter.* echoes only caller-supplied inputs; backend defaults do not round-trip.
  • Shared CLI helpers (src/commands/code/code-nav-cli-helpers.ts) cover addressing, numeric-option parsing, error rendering, and the "registry-prefix looks like a spec" ambiguity guard.
  • Binary-file handling on read_file: isBinary: true + content omitted (not null). Agents branch on the flag.
  • grep_file substring-only — regex is not supported. The shared GREP_PATTERN_SEMANTICS_NOTE string is referenced verbatim from the MCP tool description, the MCP pattern argument's describe, and the CLI help text so the three surfaces never disagree.
  • Regex-attempt heuristic fires a terminal-only nudge on zero-match results when the pattern contains deliberate regex signals (\b-class escapes, escaped metacharacters, character classes, lookaround / named groups / inline flags, brace quantifiers). Deliberately excludes bare . / * / ? / ^ / $ / | / () — too common in ordinary code.

Plain / verbose CLI contract

Each command has a grep-style plain mode and a human-readable verbose mode:

  • code files plain = bare paths, one per line; --verbose adds header + language / file-type / size annotations.
  • code read plain = raw file bytes, verbatim (cat-style); --verbose adds the <path> · <language> · lines <N-M> of <total> header and a right-aligned line-number gutter.
  • code grep plain = matching lines only (grep-style); --verbose adds header + gutter + > match marker. --context <n> (default 0) adds surrounding lines; nearby matches whose contexts touch merge into a single block with no duplicated lines (grep -C / rg -C convention), distinct blocks separated by --.

Plain-mode truncation warnings (hasMore), zero-result hints, and the regex-attempt nudge all route to stderr so pipes stay uncorrupted. Verbose mode renders the same text inline.

code grep exit codes match grep(1):

Exit Meaning
0 at least one match
1 zero matches (plain and --json — scripting stays consistent across surfaces)
2 error (missing file, indexing, invalid args, backend failure)

The other commands stay on the conventional 0 success / 1 error split.

list_files.path_prefix is a literal directory prefix

Not a glob — *.ts does not match. Both surfaces call this out explicitly in their help text / MCP description so agents reaching for *.ts or **/*.py aren't silently confused by zero-result responses. A glob / fileTypes enhancement is filed with the backend.

Positionals

  • code files [spec] [path-prefix]
  • code read [spec] <path>
  • code grep [spec] <pattern> <path>

All three mirror each other so copy-paste between them works. In --repo-url mode the spec positional is replaced by the URL flags; a registry-prefix ambiguity guard on code files catches code files npm:express --repo-url … typos that would otherwise silently interpret the spec as a path prefix.

Testing

  • 1327 tests pass (bun test). Full-surface coverage per tool: service executor, request builder, envelope builder, terminal formatter, MCP tool, CLI action, and CLI↔MCP parity.
  • Parity tests assert byte-identical JSON output between CLI --json and MCP text payload; INVALID_ARGUMENT fixtures assert the full envelope shape (including retryable: false).
  • Plain/verbose split, stderr routing, exit codes, and block-merging edge cases (unsorted matches, match/context line collision, null context arrays) all pinned by unit tests.
  • Typecheck, lint, build clean.

Live-smoke (production pkgseer)

Chain verified end-to-end against npm:express: code files npm:express lib enumerates paths, code read npm:express lib/express.js --lines 1-40 reads content verbatim, code grep npm:express exports lib/express.js --context 2 returns merged, deduplicated context blocks. Binary-file sentinel, NOT_FOUND → code-files hint, INDEXING enrichment, and the regex-attempt nudge have all been exercised.

Test plan

  • bun test passes (1327 / 0)
  • bun run typecheck / bun run build / bun run lint clean
  • Live-smoke every tool + error path + chain workflow against production pkgseer
  • Full-bundle plan-reviewer + codex-reviewer rounds applied
  • UX pass: pipe-friendly plain mode, grep-style exit codes, merged context blocks, positional parity across commands
  • Documentation: per-command subsections in docs/implementation/cli-commands.md; shared design in docs/implementation/tools.md and mcp-cli-parity.md

🤖 Generated with Claude Code

jlitola added a commit that referenced this pull request Apr 21, 2026
…ning, docs

Applied plan-reviewer + codex findings on the file-exploration
bundle (PR #20). No behaviour changes — reviewers flagged that
our UX invariants were only hand-verified, and that the grep MCP
description lagged the new default-0 contract.

Tests added:
- \`code grep\` exit-code contract pinned: 0 on match, 1 on zero
  matches (plain + --json), 2 on error. Catches any future drift
  of the grep-style scripting contract.
- \`code grep\` plain-mode stderr routing: \`hasMore\` truncation
  warning and regex-shaped pattern nudge both asserted on stderr
  (not stdout) so pipe-cleanliness regressions surface in CI.
- \`mergeMatchBlocks\` edge cases: unsorted-matches input, match
  line colliding with another match's context entry (the
  \`isMatch: true\` override path), and null/empty
  contextBefore/After — all now have unit coverage.

Docs:
- New \`### githits code files\` / \`code read\` / \`code grep\`
  subsections in cli-commands.md matching the \`pkg\` family
  pattern. Each calls out stdout vs stderr routing, exit codes,
  and the plain/verbose split — the non-obvious UX commitments
  we locked in during the pass.
- MCP \`grep_file\` description corrected: previously said
  "Returns matches with surrounding context lines" which
  contradicted the default-0 behaviour — now says "Returns
  matches only by default — pass context_lines for surrounding
  lines (0–10, default 0)."
…ep_file

Ships three capability-gated tools that complete the "which files
does this dependency ship? → read one → search inside it" chain
for agents investigating third-party source. Each is available as
both a CLI subcommand (`githits code files` / `code read` /
`code grep`) and an MCP tool (`list_files` / `read_file` /
`grep_file`). All three sit behind the existing `code_navigation`
capability gate, dual-addressed (`registry`+`package_name` XOR
`repo_url`+`git_ref`), and share the indexing lifecycle used by
the shipped `search_symbols` tool.

## The workflow

```
code files npm:express lib                          # discover paths
code read  npm:express lib/express.js --lines 1-40  # read content
code grep  npm:express exports lib/express.js       # find a string
```

The `path` field is shared verbatim across the three envelopes
(`list_files.files[].path` → `read_file({path})` /
`grep_file({path})`) so chaining has no rename friction.

## Design

- **Dual addressing** via the shipped `codeTargetSchema` +
  `resolveCodeTarget`. XOR enforced; `INVALID_ARGUMENT` envelope
  carries `retryable: false`.
- **Indexing lifecycle → typed error.** GraphQL
  `PACKAGE_INDEXING` errors and data-path
  `indexingStatus: "INDEXING"` sentinels both collapse to
  `{code: "INDEXING", retryable: true, details: {indexingRef,
  availableVersions}}`. Default wait 20 s (`DEFAULT_WAIT_TIMEOUT_MS`
  in `src/shared/code-navigation-defaults.ts`), overridable by
  `--wait` / `wait_timeout_ms`.
- **Data-first envelopes.** No indexing metadata on the success
  path (the service promotes INDEXING to a typed error before
  the envelope builder runs). `filter.*` echoes only
  caller-supplied inputs.
- **Shared CLI helpers** (`src/commands/code/code-nav-cli-helpers.ts`)
  cover addressing, numeric-option parsing, error rendering,
  and the "registry-prefix looks like a spec" ambiguity guard.
- **Binary-file handling** on `read_file`: `isBinary: true` +
  `content` omitted (not null). Agents branch on the flag.
- **`grep_file` substring-only** — regex is not supported. The
  shared `GREP_PATTERN_SEMANTICS_NOTE` string is referenced
  verbatim from the MCP tool description, the MCP `pattern`
  argument's `describe`, and the CLI help text so the three
  surfaces never disagree.
- **Regex-attempt heuristic** fires a terminal-only nudge on
  zero-match results when the pattern contains deliberate
  regex signals (`\b`-class escapes, escaped metacharacters,
  character classes, lookaround / named groups / inline flags,
  brace quantifiers). Deliberately excludes bare `.` / `*` /
  `?` / `^` / `$` / `|` / `()` — too common in ordinary code.

## Plain / verbose CLI contract

Each command has a grep-style plain mode and a
human-readable verbose mode:

- `code files` plain = bare paths (one per line, pipe-friendly);
  `--verbose` adds header + language / file-type / size
  annotations.
- `code read` plain = raw file bytes (verbatim, `cat`-style);
  `--verbose` adds the `<path> · <language> · lines <N-M> of
  <total>` header and a right-aligned line-number gutter.
- `code grep` plain = matching lines only (`grep`-style);
  `--verbose` adds header + gutter + `>` match marker.
  `--context <n>` (default 0) adds surrounding lines; nearby
  matches whose contexts touch merge into a single block with
  no duplicated lines (`grep -C` / `rg -C` convention), blocks
  separated by `--`.

Plain-mode truncation warnings (`hasMore`), zero-result hints,
and the regex-attempt nudge all route to **stderr** so pipes
stay uncorrupted. Verbose mode renders the same text inline.

`code grep` adopts grep's exit codes: `0` on match, `1` on zero
matches (including `--json` mode so scripting stays consistent),
`2` on error. The other commands stay on the conventional
`0` success / `1` error split.

## `list_files` `path_prefix` is a literal directory prefix

Not a glob — `*.ts` does not match. Both surfaces call this out
explicitly in their help text / MCP description so agents
reaching for `*.ts` or `**/*.py` aren't silently confused by
zero-result responses.

## Testing

- 1327 tests pass (`bun test`). Full-surface coverage per tool:
  service executor, request builder, envelope builder, terminal
  formatter, MCP tool, CLI action, CLI↔MCP parity.
- Parity tests assert byte-identical JSON output between CLI
  `--json` and MCP text payload; `INVALID_ARGUMENT` fixtures
  assert the full envelope shape (including `retryable: false`).
- Plain/verbose split, stderr routing, exit codes, and
  block-merging edge cases (unsorted matches, match/context
  line collision, null context arrays) all pinned by unit
  tests — regressions surface in CI, not in an agent's terminal.
- Typecheck, lint, build clean.

## Live-smoke (production pkgseer)

Chain verified end-to-end against `npm:express`: `code files
npm:express lib` enumerates paths, `code read npm:express
lib/express.js --lines 1-40` reads content verbatim, `code
grep npm:express exports lib/express.js --context 2` returns
merged, deduplicated context blocks. Binary-file sentinel,
NOT_FOUND → code-files hint, INDEXING enrichment, regex-attempt
nudge all exercised.
@jlitola
jlitola force-pushed the feat/code-nav-file-exploration branch from 674e3fc to cbe6cd8 Compare April 21, 2026 12:57
@jlitola jlitola changed the title feat(code-nav): P5 file-exploration bundle — list_files + read_file + grep_file feat(code-nav): file-exploration bundle — list_files + read_file + grep_file Apr 21, 2026
@jlitola
jlitola merged commit cf8f28d into main Apr 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant