# Plan: Add `context_lines` parameter to `grep` tool

## Goal

Let the LLM request ±N surrounding lines around each grep match, mirroring `grep -C N`.

## Current state

- **Schema** (`swival/tools.py:217–256`): `grep` has `pattern`, `path`, `include`, `case_insensitive`. No context parameter.
- **Implementation** (`swival/tools.py:837–1008`): `_grep()` walks files, collects `(filepath, line_no, line_text, mtime)` tuples for matching lines only. Output is grouped by file, formatted as `Line N: text`.
- **Dispatch** (`swival/tools.py:2393–2402`): passes `pattern`, `path`, `include`, `case_insensitive` to `_grep()`.
- **Compaction** (`swival/agent.py:487–491`): `compact_tool_result()` estimates match count via `content.count("\n")`, which assumes one line per match.
- **Docs**: `docs.md/tools.md:47–49` describes grep parameters; `system_prompt.txt:14` lists grep with its current parameters.
- **Tests**: `tests/test_list_grep.py::TestGrep` (main), plus grep tests in `test_yolo.py`, `test_add_dir.py`, `test_compact.py`, `test_think.py`.

## Changes

### 1. Add `context_lines` to the tool schema (`swival/tools.py` ~line 247)

```python
"context_lines": {
    "type": "integer",
    "description": "Number of surrounding lines to show before and after each match.",
    "minimum": 0,
    "default": 0,
},
```

Insert after `case_insensitive`. Not required.

### 2. Update `_grep()` signature (`swival/tools.py` ~line 837)

Add `context_lines: int = 0` parameter.

### 3. Change match collection logic

Currently matches are individual `(filepath, line_no, line_text, mtime)` tuples. With context_lines > 0, we need surrounding lines too.

**Approach — expand at collection time**: After `text.splitlines()`, when a line matches, capture the context window `[max(0, i-context_lines) : i+context_lines+1]` as a list of `(line_no, line_text, is_match)` triples. Store as `(filepath, match_line_no, context_block, mtime)`.

- When `context_lines == 0`, `context_block` is just `[(line_no, line_text, True)]` — same data, just wrapped.

### 4. Cap behavior and merge ordering

`MAX_GREP_MATCHES` caps **matching lines only**. The pipeline is: collect all matches → sort by mtime → cap to `MAX_GREP_MATCHES` → **then** expand context windows and merge overlapping blocks within each file. Merging must happen after sort + cap; if you merge first, context from matches that should be dropped can leak into the final output. `MAX_OUTPUT_BYTES` constrains the final expanded output — this is what prevents context from blowing up the result.

### 5. Update output formatting (`swival/tools.py` ~line 971)

Currently:
```
file.py:
  Line 10: matching text
```

With context_lines > 0, format like:
```
file.py:
  Line 8:   context before
  Line 9:   context before
  Line 10:  matching text  <<<
  Line 11:  context after
  Line 12:  context after
  --
  Line 30:  matching text  <<<
```

- Use `--` separator between non-contiguous context blocks (same convention as `grep -C`).
- Mark matching lines with ` <<<` suffix so the LLM can distinguish matches from context.
- When `context_lines == 0`, no markers, no separators — output is identical to today.

### 6. Update dispatch (`swival/tools.py` ~line 2393)

Pass `context_lines=args.get("context_lines", 0)` to `_grep()`.

### 7. Fix compaction match estimate (`swival/agent.py` ~line 487)

`content.count("\n")` currently approximates match count. With context lines, separators, and markers this becomes misleading. Fix: count lines that end with ` <<<` when context is active, or parse the `Found N matches` header line that `_grep()` already emits. The header is the most reliable — extract the number from the first line (`"Found \d+ matches"`).

### 8. Update system prompt (`system_prompt.txt` ~line 14)

Add `context_lines` to the grep tool description:

```
- `grep`: Search file contents for a regex pattern. Use `include` to filter by filename glob,
  `path` to scope the directory, `case_insensitive` for case-insensitive matching,
  and `context_lines` for surrounding context around each match.
```

### 9. Update docs (`docs.md/tools.md` ~line 47)

Expand the grep section to mention `context_lines`:

```
`grep` searches file contents with Python regular expressions. Matches are grouped by file,
include line numbers, and are sorted by file recency so the newest files are surfaced first.
You can narrow by directory with `path` and by filename glob with `include` (supports
`**/*.ext` patterns). Set `context_lines` to show surrounding lines around each match;
when active, matching lines are marked with ` <<<` to distinguish them from context.
```

### 10. Tests (`tests/test_list_grep.py`)

Add to `TestGrep`:

- **`test_grep_context_lines_zero`** — default behavior unchanged, no markers.
- **`test_grep_context_lines_one`** — single match, context_lines=1, verify ±1 lines appear with `<<<` marker on match.
- **`test_grep_context_lines_overlapping`** — two close matches whose context windows overlap; verify lines aren't duplicated and `--` separator is absent between them.
- **`test_grep_context_lines_at_file_edges`** — match on line 1 or last line; verify no index errors, context is truncated gracefully.
- **`test_grep_context_lines_marker`** — verify matching lines have ` <<<` marker, context lines don't.
- **`test_grep_context_lines_multi_file`** — context works across multiple files.
- **`test_grep_context_lines_byte_cap`** — context lines count toward `MAX_OUTPUT_BYTES` but not `MAX_GREP_MATCHES`.
- **`test_grep_context_lines_negative`** — negative value treated as 0.

Add to compaction tests (`tests/test_compact.py`):

- **`test_grep_compaction_with_context`** — verify compacted summary uses the header count, not newline count, when context is present.

## Edge cases

- `context_lines` < 0 → schema declares `"minimum": 0`; runtime clamps as a defensive fallback.
- Very large `context_lines` values → `MAX_OUTPUT_BYTES` caps the expanded output.
- Single-file mode (`swival/tools.py` ~line 882) needs the same context logic as directory mode.

## Files touched

| File | Change |
|------|--------|
| `swival/tools.py` | Schema, `_grep()` impl, dispatch |
| `swival/agent.py` | `compact_tool_result()` grep case |
| `swival/system_prompt.txt` | grep description |
| `docs.md/tools.md` | grep documentation |
| `tests/test_list_grep.py` | New context_lines tests |
| `tests/test_compact.py` | Compaction accuracy test |
