bots-garden/mini-mepublic Fork
⑂ feature/zed-skill-selector
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
🛟 Updated. 9136e38 · on feature/zed-skill-selector · k33g · Sep 21, 2026
tools.md · 182 lines · 13.7 KBmarkdown
Blame HistoryOpen raw

Reference: built-in tools

Neutral description of the tools declared to the model. A tool never returns a Go error for an ordinary failure: the failure text is returned to the model as the tool's output. Every tool records its call in the loop detector; the same tool, input and output three times in a row appends [LOOP_DETECTED: …] to the output.

bash

Declared when bashTool: true (the default).

Field Type Description
command string The shell command to run.
Aspect Behaviour
Shell bash -c <command>, a new shell per call, no stdin.
Working directory The process directory in terminal mode; the session cwd in ACP mode.
Timeout 30 s; the output then ends with [error: 30s timeout exceeded].
Cancellation The command belongs to the turn: cancelling the turn (Esc, Ctrl-C, /abort, or asking something else) kills it, and the output ends with [error: cancelled].
Background jobs The output is collected through a temporary file, not a pipe, so the tool returns as soon as the shell exits even when the command left a job running (nohup serve &). The job keeps running — that is usually the point — and writes into a file nobody reads. Through a pipe, os/exec waits for every process holding the write end, so such a command blocked the turn for ever.
Very large output Read back from both ends (1 MiB each) before maxOutput truncation, with [... output too large to read back in full ...] between them.
Output stdout and stderr merged, truncated to maxOutput characters.
Non-zero exit Output ends with [exit code: exit status N].
Terminal display 🛠️ bash: <command> before running, then up to previewLines lines dimmed and indented, or (no output).
ACP Tool call of kind execute; permission requested before running; a refusal returns [command rejected by the user] with status failed.
Window mode One ‣ bash <command> line with status, time and folded output. With permissions: ask, waits for a/s/r; a refusal returns the same text as in ACP.

read_skill

Declared only when skillsDir holds at least one skill (<name>.md or <name>/SKILL.md).

Field Type Description
name string Skill name, exactly as listed in the tool description.
Aspect Behaviour
Description Fixed sentence followed by the catalogue: one <name> — <description> line per skill, sorted by name.
Output The full markdown of <skillsDir>/<name>.md, or of <skillsDir>/<name>/SKILL.md when there is no flat file, truncated to maxOutput.
Unknown name No skill named "x". Available skills: a, b, c
Path safety Only the base name of name is used; no directory traversal.
Terminal display 📖 read_skill: <name>; the content is not echoed.
ACP Tool call of kind read; no permission dialog.

read_file

Declared when editTools: true.

Field Type Default Description
path string — File path, relative to the working directory.
start int 0 First line (1-based); 0 = from the start.
end int 0 Last line (1-based, inclusive); 0 = to the end.
numbered bool false Prefix each line with its number.
Aspect Behaviour
Missing file <path> does not exist. Create it with write_file
Output The lines, truncated to maxOutput; the final newline is kept only when the end of the file is shown.
ACP Kind read, with the absolute path as location.

write_file

Declared when editTools: true.

Field Type Description
path string File path, relative to the working directory; parent directories are created.
content string The whole content of the file.
Aspect Behaviour
Existing file Entirely replaced.
Identical content Nothing is written; the result says so.
Output A one-line headline and the diff against the previous content.
ACP Kind edit; permission requested; the change travels as a diff (new-file diff when created).

edit_file

Declared when editTools: true.

Field Type Default Description
path string — Path of an existing file.
edits array of {old, new} — Replacements, all resolved against the original file.
edits[].old string — Exact text to find, present exactly once. Whitespace is forgiven — see How old is matched.
edits[].new string — Replacement; empty deletes the matched text.
dry_run bool false Report what would change, write nothing.
Rule Consequence when broken
old appears exactly once Refused: not found, or ambiguous — old text appears N times (lines 11, 20) — to change ONE of them, include its surrounding lines in old until the text is unique; to change several, send one edit per place….
Two edits must not overlap, nest or repeat Refused.
Edits are resolved against the original text, never in cascade —
Any refused edit Nothing is written.
Aspect Behaviour
Output A one-line headline (edits applied, lines added and deleted, first changed line, and · N matched ignoring whitespace when any edit needed a relaxed match) and the diff with line numbers.
ACP Kind edit; permission requested; the change travels as a diff with old and new text.

How old is matched

Three passes, strictest first; the first pass that finds anything at all wins, so a file that contains the text verbatim is never matched any other way.

Pass What it forgives Reported as
1. Exact Nothing. old is searched byte for byte. —
2. Edges The whitespace around old: an indent the model kept on a single line, a leading or trailing newline. The same padding comes off new when new carries it, so a new that deliberately adds a blank line still adds it. matched ignoring whitespace
3. Indent Every line's leading and trailing whitespace, for an old spanning two lines or more. This is the tabs-versus-spaces case: a model retypes a Go block with four spaces where the file has a tab. matched ignoring whitespace

Two things are not relaxed:

  • Uniqueness. A loose match must still be the only one. Two of them are refused exactly as two exact ones are, with the same message and the same line numbers — that rule is what makes an edit verifiable.
  • The file's indentation. Before writing, new is re-indented to the file's own base indent and unit, so a tab-indented file does not end up with a block of spaces in the middle of it. A blank line stays empty rather than becoming a line of spaces.

The headline then ends with · N matched ignoring whitespace. It is reported rather than hidden: a model told its text did not match exactly writes the next one better.

When nothing matches, the refusal shows the region of the file that came closest, with real line numbers — half the lines have to line up before an excerpt is offered, since pointing at an innocent part of the file is worse than saying nothing:

main.go: edit #1: old text not found — it must match the file exactly, including indentation and line breaks:
  │ func main() {
  │     count := 2
  │     print(count)
  │ }
the closest the file comes to it — copy these lines:
  │ 3 │ func main() {
  │ 4 │ 	count := 1
  │ 5 │ 	print(count)
  │ 6 │ }

lsp

Declared when lsp.servers lists at least one server whose command is on the PATH at start-up. A listed server that is not found is skipped with a [warning: lsp server <name> (<command>) not found on the PATH — skipped]; when none is found the tool is not declared and the banner says so.

Field Type Default Description
action string — definition, references, hover, symbols, diagnostics or rename.
path string — File path, relative to the working directory. Its extension picks the server.
symbol string "" The identifier, as written in the code. Stray quotes and punctuation are cut off and a receiver. or pkg. prefix dropped (Greet', and person.Greet both mean Greet). Its occurrences in the file are tried in order — code lines first, comment lines last — until the server answers. For definition, references, hover, rename.
line int 0 1-based line. With symbol: restricts the search to that line. With column: the exact position.
column int 0 1-based column, in characters, pointing inside the identifier. Only needed without symbol.
new_name string "" The new identifier, for rename. Cleaned like symbol. When it is empty and symbol holds several words (Greet 1 12 Hello — a small model writing symbol, line, column and new name into one string), the first identifier is the symbol and the last the new name.
Aspect Behaviour
Description Names the servers and their extensions (Servers available: gopls (.go), …) and the six actions.
definition, references One path:line:column <source line> per location, sorted by path then position, duplicates removed; paths inside the project are relative, others absolute. references is prefixed with N reference(s): and includes the declaration.
hover The server's text as it sent it (signature, type, documentation; Markdown for most servers).
symbols One <kind> <name> [<detail>] line N per symbol, children indented two spaces.
diagnostics The file is synced, then the call waits up to 3 s for the server's publication. N diagnostic(s) in <path>: then one <severity> path:line:column <message> [<source>] per finding; no diagnostics: <path> is clean; (as last published) when the file did not change since the server last spoke; no diagnostics received for <path> within 3s (…) when it never did.
rename Applies the server's edits to the files on disk (UTF-16 positions converted), tells the server the new contents, and answers renamed to <new>: N edit(s) in M file(s) followed by one path: N edit(s) line per file. Without new_name: [lsp: rename needs new_name — call lsp again with {"action": "rename", "path": …, "symbol": …, "new_name": "<the new identifier>"}; do not use edit_file for a rename].
Positions 1-based lines and columns at the tool's boundary, 0-based UTF-16 on the wire. A position past the end of a line or of the file is refused with the count.
File sync Before every request the file is read from disk and sent as didOpen (first time) or a whole-document didChange (when its content moved): edits made with edit_file are seen without any extra step.
Server lifecycle Started on the first call for one of its extensions, in the project root (the directory mm runs in), kept for the session, stopped (shutdown, exit, then the process) when mm exits. A server that died is started again on the next call.
Failures Always text, never a Go error: [lsp: no language server for "x.md" — configured: gopls (.go)], [lsp: "foo" not found in main.go], [lsp: column 40 is past the end of line 3 (15 characters)], [lsp: gopls: hover failed: …], [lsp: no definition found for the symbol at main.go:5:4].
Terminal display 🔎 lsp: definition main.go greet (or …:9:14 with a position, rename main.go greet → hello), then the answer echoed like a command's output.
ACP / window Kind read for navigation, no permission; rename is kind edit and asks for permission like edit_file ([rename rejected by the user] when refused).
Counter 🔎 N lsp call(s) in the recap line (see below).

Loop detection

Every tool records (tool, input, output) in the loop detector after each call.

Situation Behaviour
The same triple 3 times in a row The tool's output gets [LOOP_DETECTED: You have attempted the same action multiple times with the same result. Please try a different approach or provide more information.] appended. Nothing stops.
3 more identical calls after that (6 in a row) The tool returns an error instead of text and the turn ends: [error: the model repeated the same edit_file call 6 times in a row with the same failing result — turn stopped by the loop detector; rephrase the request, or fix what the tool kept reporting]. The history keeps everything that ran; the error is recorded in the session file's errors array (where: generation).
A different call The count starts again. /new resets the detector; /compact does not.
A tool call whose streamed arguments repeat a pattern (the last 256 characters are one 4–64-character pattern repeated) or exceed 256 KB The generation is stopped at once: [error: the model's tool call ran away: its arguments repeat the same pattern (N characters so far: …) — generation stopped; rephrase the request, or try another model]. Without it a derailed model composes garbage for minutes while the screen shows "Thinking" (the watchdog is fed by every delta). While a tool call streams, the spinner says Composing a tool call.

The thresholds (3, then 3 more) are set in main.go; maxTurns remains the outer bound of a turn.

Counters shown after a turn

Counter Counts
⚙ N command(s) bash tool responses in the turn.
📝 N file op(s) read_file, write_file, edit_file responses.
📖 N skill(s) read_skill responses.
🔎 N lsp call(s) lsp responses.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# Reference: built-in tools

> Neutral description of the tools declared to the model. A tool never returns a Go error for an ordinary failure: the failure text is returned to the model as the tool's output. Every tool records its call in the loop detector; the same tool, input and output three times in a row appends `[LOOP_DETECTED: …]` to the output.

## `bash`

Declared when `bashTool: true` (the default).

| Field | Type | Description |
|-------|------|-------------|
| `command` | string | The shell command to run. |

| Aspect | Behaviour |
|--------|-----------|
| Shell | `bash -c <command>`, a new shell per call, no stdin. |
| Working directory | The process directory in terminal mode; the session `cwd` in ACP mode. |
| Timeout | 30 s; the output then ends with `[error: 30s timeout exceeded]`. |
| Cancellation | The command belongs to the turn: cancelling the turn (`Esc`, `Ctrl-C`, `/abort`, or asking something else) kills it, and the output ends with `[error: cancelled]`. |
| Background jobs | The output is collected through a temporary file, not a pipe, so the tool returns as soon as the shell exits even when the command left a job running (`nohup serve &`). The job keeps running — that is usually the point — and writes into a file nobody reads. Through a pipe, `os/exec` waits for *every* process holding the write end, so such a command blocked the turn for ever. |
| Very large output | Read back from both ends (1 MiB each) before `maxOutput` truncation, with `[... output too large to read back in full ...]` between them. |
| Output | stdout and stderr merged, truncated to `maxOutput` characters. |
| Non-zero exit | Output ends with `[exit code: exit status N]`. |
| Terminal display | `🛠️  bash: <command>` before running, then up to `previewLines` lines dimmed and indented, or `(no output)`. |
| ACP | Tool call of kind `execute`; permission requested before running; a refusal returns `[command rejected by the user]` with status `failed`. |
| Window mode | One `‣ bash  <command>` line with status, time and folded output. With `permissions: ask`, waits for `a`/`s`/`r`; a refusal returns the same text as in ACP. |

## `read_skill`

Declared only when `skillsDir` holds at least one skill (`<name>.md` or `<name>/SKILL.md`).

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Skill name, exactly as listed in the tool description. |

| Aspect | Behaviour |
|--------|-----------|
| Description | Fixed sentence followed by the catalogue: one `  <name> — <description>` line per skill, sorted by name. |
| Output | The full markdown of `<skillsDir>/<name>.md`, or of `<skillsDir>/<name>/SKILL.md` when there is no flat file, truncated to `maxOutput`. |
| Unknown name | `No skill named "x". Available skills: a, b, c` |
| Path safety | Only the base name of `name` is used; no directory traversal. |
| Terminal display | `📖 read_skill: <name>`; the content is not echoed. |
| ACP | Tool call of kind `read`; no permission dialog. |

## `read_file`

Declared when `editTools: true`.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `path` | string | — | File path, relative to the working directory. |
| `start` | int | `0` | First line (1-based); `0` = from the start. |
| `end` | int | `0` | Last line (1-based, inclusive); `0` = to the end. |
| `numbered` | bool | `false` | Prefix each line with its number. |

| Aspect | Behaviour |
|--------|-----------|
| Missing file | `<path> does not exist. Create it with write_file` |
| Output | The lines, truncated to `maxOutput`; the final newline is kept only when the end of the file is shown. |
| ACP | Kind `read`, with the absolute path as location. |

## `write_file`

Declared when `editTools: true`.

| Field | Type | Description |
|-------|------|-------------|
| `path` | string | File path, relative to the working directory; parent directories are created. |
| `content` | string | The whole content of the file. |

| Aspect | Behaviour |
|--------|-----------|
| Existing file | Entirely replaced. |
| Identical content | Nothing is written; the result says so. |
| Output | A one-line headline and the diff against the previous content. |
| ACP | Kind `edit`; permission requested; the change travels as a diff (new-file diff when created). |

## `edit_file`

Declared when `editTools: true`.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `path` | string | — | Path of an existing file. |
| `edits` | array of `{old, new}` | — | Replacements, all resolved against the original file. |
| `edits[].old` | string | — | Exact text to find, present exactly once. Whitespace is forgiven — see *How `old` is matched*. |
| `edits[].new` | string | — | Replacement; empty deletes the matched text. |
| `dry_run` | bool | `false` | Report what would change, write nothing. |

| Rule | Consequence when broken |
|------|-------------------------|
| `old` appears exactly once | Refused: not found, or ambiguous — `old text appears N times (lines 11, 20) — to change ONE of them, include its surrounding lines in old until the text is unique; to change several, send one edit per place…`. |
| Two edits must not overlap, nest or repeat | Refused. |
| Edits are resolved against the original text, never in cascade | — |
| Any refused edit | Nothing is written. |

| Aspect | Behaviour |
|--------|-----------|
| Output | A one-line headline (edits applied, lines added and deleted, first changed line, and ` · N matched ignoring whitespace` when any edit needed a relaxed match) and the diff with line numbers. |
| ACP | Kind `edit`; permission requested; the change travels as a diff with old and new text. |

### How `old` is matched

Three passes, strictest first; **the first pass that finds anything at all wins**, so a file that contains the text verbatim is never matched any other way.

| Pass | What it forgives | Reported as |
|---|---|---|
| 1. Exact | Nothing. `old` is searched byte for byte. | — |
| 2. Edges | The whitespace *around* `old`: an indent the model kept on a single line, a leading or trailing newline. The same padding comes off `new` when `new` carries it, so a `new` that deliberately adds a blank line still adds it. | `matched ignoring whitespace` |
| 3. Indent | Every line's leading and trailing whitespace, for an `old` spanning two lines or more. This is the tabs-versus-spaces case: a model retypes a Go block with four spaces where the file has a tab. | `matched ignoring whitespace` |

Two things are **not** relaxed:

- **Uniqueness.** A loose match must still be the only one. Two of them are refused exactly as two exact ones are, with the same message and the same line numbers — that rule is what makes an edit verifiable.
- **The file's indentation.** Before writing, `new` is re-indented to the file's own base indent and unit, so a tab-indented file does not end up with a block of spaces in the middle of it. A blank line stays empty rather than becoming a line of spaces.

The headline then ends with ` · N matched ignoring whitespace`. It is reported rather than hidden: a model told its text did not match exactly writes the next one better.

When nothing matches, the refusal shows the region of the file that came closest, with real line numbers — half the lines have to line up before an excerpt is offered, since pointing at an innocent part of the file is worse than saying nothing:

```
main.go: edit #1: old text not found — it must match the file exactly, including indentation and line breaks:
  │ func main() {
  │     count := 2
  │     print(count)
  │ }
the closest the file comes to it — copy these lines:
  │ 3 │ func main() {
  │ 4 │ 	count := 1
  │ 5 │ 	print(count)
  │ 6 │ }
```

## `lsp`

Declared when `lsp.servers` lists at least one server whose command is on the `PATH` at start-up. A listed server that is not found is skipped with a `[warning: lsp server <name> (<command>) not found on the PATH — skipped]`; when none is found the tool is not declared and the banner says so.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `action` | string | — | `definition`, `references`, `hover`, `symbols`, `diagnostics` or `rename`. |
| `path` | string | — | File path, relative to the working directory. Its extension picks the server. |
| `symbol` | string | `""` | The identifier, as written in the code. Stray quotes and punctuation are cut off and a `receiver.` or `pkg.` prefix dropped (`Greet', ` and `person.Greet` both mean `Greet`). Its occurrences in the file are tried in order — code lines first, comment lines last — until the server answers. For `definition`, `references`, `hover`, `rename`. |
| `line` | int | `0` | 1-based line. With `symbol`: restricts the search to that line. With `column`: the exact position. |
| `column` | int | `0` | 1-based column, in characters, pointing inside the identifier. Only needed without `symbol`. |
| `new_name` | string | `""` | The new identifier, for `rename`. Cleaned like `symbol`. When it is empty and `symbol` holds several words (`Greet 1 12 Hello` — a small model writing symbol, line, column and new name into one string), the first identifier is the symbol and the last the new name. |

| Aspect | Behaviour |
|--------|-----------|
| Description | Names the servers and their extensions (`Servers available: gopls (.go), …`) and the six actions. |
| `definition`, `references` | One `path:line:column  <source line>` per location, sorted by path then position, duplicates removed; paths inside the project are relative, others absolute. `references` is prefixed with `N reference(s):` and includes the declaration. |
| `hover` | The server's text as it sent it (signature, type, documentation; Markdown for most servers). |
| `symbols` | One `<kind> <name>  [<detail>]  line N` per symbol, children indented two spaces. |
| `diagnostics` | The file is synced, then the call waits up to 3 s for the server's publication. `N diagnostic(s) in <path>:` then one `<severity> path:line:column <message> [<source>]` per finding; `no diagnostics: <path> is clean`; ` (as last published)` when the file did not change since the server last spoke; `no diagnostics received for <path> within 3s (…)` when it never did. |
| `rename` | Applies the server's edits to the files on disk (UTF-16 positions converted), tells the server the new contents, and answers `renamed to <new>: N edit(s) in M file(s)` followed by one `path: N edit(s)` line per file. Without `new_name`: `[lsp: rename needs new_name — call lsp again with {"action": "rename", "path": …, "symbol": …, "new_name": "<the new identifier>"}; do not use edit_file for a rename]`. |
| Positions | 1-based lines and columns at the tool's boundary, 0-based UTF-16 on the wire. A position past the end of a line or of the file is refused with the count. |
| File sync | Before every request the file is read from disk and sent as `didOpen` (first time) or a whole-document `didChange` (when its content moved): edits made with `edit_file` are seen without any extra step. |
| Server lifecycle | Started on the first call for one of its extensions, in the project root (the directory `mm` runs in), kept for the session, stopped (`shutdown`, `exit`, then the process) when `mm` exits. A server that died is started again on the next call. |
| Failures | Always text, never a Go error: `[lsp: no language server for "x.md" — configured: gopls (.go)]`, `[lsp: "foo" not found in main.go]`, `[lsp: column 40 is past the end of line 3 (15 characters)]`, `[lsp: gopls: hover failed: …]`, `[lsp: no definition found for the symbol at main.go:5:4]`. |
| Terminal display | `🔎 lsp: definition main.go greet` (or `…:9:14` with a position, `rename main.go greet → hello`), then the answer echoed like a command's output. |
| ACP / window | Kind `read` for navigation, no permission; `rename` is kind `edit` and asks for permission like `edit_file` (`[rename rejected by the user]` when refused). |
| Counter | `🔎 N lsp call(s)` in the recap line (see below). |

## Loop detection

Every tool records `(tool, input, output)` in the loop detector after each call.

| Situation | Behaviour |
|-----------|-----------|
| The same triple 3 times in a row | The tool's output gets `[LOOP_DETECTED: You have attempted the same action multiple times with the same result. Please try a different approach or provide more information.]` appended. Nothing stops. |
| 3 more identical calls after that (6 in a row) | The tool returns an error instead of text and the turn ends: `[error: the model repeated the same edit_file call 6 times in a row with the same failing result — turn stopped by the loop detector; rephrase the request, or fix what the tool kept reporting]`. The history keeps everything that ran; the error is recorded in the session file's `errors` array (`where: generation`). |
| A different call | The count starts again. `/new` resets the detector; `/compact` does not. |
| A tool call whose streamed arguments repeat a pattern (the last 256 characters are one 4–64-character pattern repeated) or exceed 256 KB | The generation is stopped at once: `[error: the model's tool call ran away: its arguments repeat the same pattern (N characters so far: …) — generation stopped; rephrase the request, or try another model]`. Without it a derailed model composes garbage for minutes while the screen shows "Thinking" (the watchdog is fed by every delta). While a tool call streams, the spinner says `Composing a tool call`. |

The thresholds (3, then 3 more) are set in `main.go`; `maxTurns` remains the outer bound of a turn.

## Counters shown after a turn

| Counter | Counts |
|---------|--------|
| `⚙ N command(s)` | `bash` tool responses in the turn. |
| `📝 N file op(s)` | `read_file`, `write_file`, `edit_file` responses. |
| `📖 N skill(s)` | `read_skill` responses. |
| `🔎 N lsp call(s)` | `lsp` responses. |