# Add tail support to `read_file`

## Motivation

Currently `read_file` supports `offset` (1-based start line) and `limit` (max lines)
for forward pagination. There's no way to read the end of a file without knowing its
total line count first. A `tail` parameter lets the model jump straight to the last N
lines — useful for log files, build output, and any situation where the interesting
content is at the bottom.

## Design

Add an integer `tail` parameter to `read_file`. When set, it returns the last N lines
of the file instead of reading from `offset`. `offset` is ignored when `tail` is
provided. `tail` must be >= 1. The schema declares `"minimum": 1` as a hint to the
LLM, and `_read_file` clamps non-positive values to 1 defensively at runtime.

Behavior summary:

| Parameters             | Result                                      |
|------------------------|---------------------------------------------|
| (default)              | Lines 1–2000 (existing behavior)            |
| `offset=50, limit=10`  | Lines 50–59 (existing behavior)             |
| `tail=20`              | Last 20 lines of the file                   |
| `tail=20, limit=5`     | First 5 of those last 20, with offset hint  |
| `tail=20, offset=3`    | `offset` ignored, same as `tail=20`         |

When `tail` exceeds the total line count, all lines are returned (no error).

Output format stays the same: `{line_number}: {content}` with correct 1-based line
numbers. When `limit` truncates the tail window, the continuation hint
`[N more lines, use offset=X to continue]` is appended. The caller must then drop
`tail` and use the returned `offset` value to continue paginating forward. Sending
`tail` again would restart from the end of the file, not advance the page.

## File changes

### 1. Update tool schema in `tools.py` (lines 12–50)

Add the `tail` parameter to the `read_file` function schema:

```python
"tail": {
    "type": "integer",
    "minimum": 1,
    "description": (
        "Return the last N lines of the file. "
        "When set, offset is ignored. "
        "To paginate within the tail, use the offset from the continuation hint "
        "in a follow-up call (without tail)."
    ),
},
```

Update the tool description to mention tail support:

```python
"description": (
    "Read the contents of a file or list a directory. "
    "For files, returns lines prefixed with line numbers. "
    "Use offset/limit to paginate forward, or tail=N to start from the last N lines. "
    "If output is truncated, a continuation hint shows the offset for the next page. "
    "For directories, returns a listing with / suffix for subdirectories."
),
```

### 2. Update `_read_file` in `tools.py` (lines 501–580)

Add a `tail: int | None = None` parameter to the function signature.

When `tail` is provided and the target is a file (not a directory), clamp it to >= 1
(the schema sets `"minimum": 1` but that's only a hint to the LLM — the runtime
doesn't validate schemas), then compute the start line from the end:

```python
if tail is not None:
    if not isinstance(tail, int):
        return f"error: tail must be an integer, got {type(tail).__name__}"
    tail = max(tail, 1)
    start = max(len(lines) - tail, 0)
else:
    start = max(offset - 1, 0)
end = start + limit
selected = lines[start:end]
```

The rest of the function (line numbering, truncation, hint) stays unchanged — it
already uses `start` and `lines_emitted` to compute the "more lines" hint correctly.

### 3. Update `dispatch()` in `tools.py` (lines 869–875)

Pass the new `tail` argument through:

```python
elif name == "read_file":
    return _read_file(
        file_path=args["file_path"],
        base_dir=base_dir,
        offset=args.get("offset", 1),
        limit=args.get("limit", 2000),
        tail=args.get("tail"),
    )
```

### 4. Add tests in `tests/test_tools.py`

Add a new test class `TestReadFileTail` alongside the existing `TestReadFilePositive`:

| Test                              | What it checks                                    |
|-----------------------------------|---------------------------------------------------|
| `test_tail_returns_last_n_lines`  | `tail=3` on a 10-line file returns lines 8–10     |
| `test_tail_exceeds_file_length`   | `tail=100` on a 5-line file returns all 5 lines   |
| `test_tail_with_limit`            | `tail=10, limit=3` returns 3 lines starting from line (total-10+1), with continuation hint |
| `test_tail_pagination_flow`       | Call with `tail=10, limit=3`, then follow up with the returned `offset` (no `tail`) to get the next page |
| `test_tail_ignores_offset`        | `tail=5, offset=1` still returns the last 5 lines |
| `test_tail_line_numbers_correct`  | Line numbers in output match actual positions      |
| `test_tail_on_directory_ignored`  | `tail` has no effect on directory listings          |
| `test_tail_nonpositive_clamped`   | `tail=0` and `tail=-3` via direct `_read_file` both behave like `tail=1` (return last line) |
| `test_tail_non_integer_returns_error` | `tail="5"` via `dispatch` returns `"error: tail must be an integer..."` (not an exception) |
| `test_tail_via_dispatch`          | End-to-end through `dispatch("read_file", ...)`    |

### 5. Update `system_prompt.txt`

Add a note about tail in the tools section so the served LLM knows about it:

```
- Use `read_file` with `tail=N` to read the last N lines of a file (useful for logs
  and build output). If the result is truncated, use the `offset` from the continuation
  hint in a follow-up call (without `tail`) to read the next page.
```

### 6. Update `CLAUDE.md`

Add mention of tail support in the `tools.py` architecture bullet, after the existing
pagination hint description.

## Verification

```sh
uv run python -m pytest tests/test_tools.py::TestReadFileTail -v
uv run python -m pytest tests/test_tools.py -v
```
