🛟 Updated.
dda11d4 parent: 8d5240d modified
.tickets/issues/0002-add-a-about-flag.yaml +55 -4 | @@ -1,14 +1,65 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 2 |
| 3 | 3 | title: Add a about flag |
| 4 | -state: open | |
| 4 | +state: closed | |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-15T22:23:07.065Z |
| 9 | -updatedAt: 2026-09-18T18:50:37.595Z | |
| 9 | +updatedAt: 2026-09-21T05:20:00.000Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | milestone: v0.0.3 |
| 12 | -body: "" | |
| 13 | -tasks: [] | |
| 12 | +body: | | |
| 13 | + ## Implementation note (Claude, 2026-09-21) | |
| 14 | + | |
| 15 | + `mm --about` prints what the program is and exits. `-about` is the same flag (Go's `flag` package takes both spellings). | |
| 16 | + | |
| 17 | + ``` | |
| 18 | + Mini Me (mm) v0.0.3 | |
| 19 | + A minimalist coding agent for local LLMs, written in Go. | |
| 20 | + | |
| 21 | + One agent loop, three front ends: | |
| 22 | + mm [config.yaml] a terminal REPL, line by line | |
| 23 | + mm -tui [config.yaml] one terminal window: conversation, input box, status bar | |
| 24 | + mm -acp the Agent Client Protocol on stdio, for Zed and other editors | |
| 25 | + | |
| 26 | + Tools the model can call: bash, read_skill, read_file, write_file, edit_file, lsp. | |
| 27 | + Model servers: Docker Model Runner, llama.cpp — anything speaking the OpenAI API. | |
| 28 | + | |
| 29 | + https://git.rickub.com/bots-garden/mini-me | |
| 30 | + Docs: docs/en (English), docs/fr (français) · mm -h for the flags | |
| 31 | + ``` | |
| 32 | + | |
| 33 | + `internal/version/about.go`, next to `Short()` and `Long()`: it is the same question asked with more room — "what am I running?" — and a binary that can answer it needs no network and no file to do so. The version shown is the one `--version` reports, by construction. | |
| 34 | + | |
| 35 | + Answered in `main.go` right after `flag.Parse()`, **before the configuration is read**, exactly as `--version` is: asking what `mm` is must work in any directory, with no config file and no model server running. | |
| 36 | + | |
| 37 | + Deliberately one screen. An `--about` that scrolls is a README in the wrong place, and the README is one link away — hence the last line rather than a copy of it. | |
| 38 | + | |
| 39 | + Tests: `internal/version/about_test.go` — the name, the version, the project URL and each front end are present; it stays under twenty lines; it does not end with a newline (the caller's `Println` adds one); and it still reports the right version on an unstamped build, which is what anyone building from source gets. | |
| 40 | + | |
| 41 | + Docs EN+FR: `reference/cli.md` (invocation line, flag table, the Version section) and the README install block. | |
| 42 | + | |
| 43 | + Not done: no `/about` slash command. The ticket says flag, and the three terminal commands that exist (`/help`, `/quit`, `/abort`) are about the session, not the program. Say so if you want one — it is a row in `internal/command`. | |
| 44 | +tasks: | |
| 45 | + - id: 1 | |
| 46 | + title: "--about prints what mm is, before the configuration is read" | |
| 47 | + state: closed | |
| 48 | + priority: none | |
| 49 | + author: | |
| 50 | + name: k33g | |
| 51 | + email: ph.charriere@gmail.com | |
| 52 | + createdAt: 2026-09-21T05:00:00.000Z | |
| 53 | + updatedAt: 2026-09-21T05:20:00.000Z | |
| 54 | + closedAt: 2026-09-21T05:20:00.000Z | |
| 55 | + - id: 2 | |
| 56 | + title: "Tests and EN+FR docs" | |
| 57 | + state: closed | |
| 58 | + priority: none | |
| 59 | + author: | |
| 60 | + name: k33g | |
| 61 | + email: ph.charriere@gmail.com | |
| 62 | + createdAt: 2026-09-21T05:00:00.000Z | |
| 63 | + updatedAt: 2026-09-21T05:20:00.000Z | |
| 64 | + closedAt: 2026-09-21T05:20:00.000Z | |
| 14 | 65 | comments: [] |
| @@ -1,14 +1,65 @@ | |||
| 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | id: 2 | 2 | id: 2 |
| 3 | title: Add a about flag | 3 | title: Add a about flag |
| 4 | -state: open | 4 | +state: closed |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-15T22:23:07.065Z | 8 | createdAt: 2026-09-15T22:23:07.065Z |
| 9 | -updatedAt: 2026-09-18T18:50:37.595Z | 9 | +updatedAt: 2026-09-21T05:20:00.000Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | milestone: v0.0.3 | 11 | milestone: v0.0.3 |
| 12 | -body: "" | 12 | +body: | |
| 13 | -tasks: [] | 13 | + ## Implementation note (Claude, 2026-09-21) |
| 14 | + | ||
| 15 | + `mm --about` prints what the program is and exits. `-about` is the same flag (Go's `flag` package takes both spellings). | ||
| 16 | + | ||
| 17 | + ``` | ||
| 18 | + Mini Me (mm) v0.0.3 | ||
| 19 | + A minimalist coding agent for local LLMs, written in Go. | ||
| 20 | + | ||
| 21 | + One agent loop, three front ends: | ||
| 22 | + mm [config.yaml] a terminal REPL, line by line | ||
| 23 | + mm -tui [config.yaml] one terminal window: conversation, input box, status bar | ||
| 24 | + mm -acp the Agent Client Protocol on stdio, for Zed and other editors | ||
| 25 | + | ||
| 26 | + Tools the model can call: bash, read_skill, read_file, write_file, edit_file, lsp. | ||
| 27 | + Model servers: Docker Model Runner, llama.cpp — anything speaking the OpenAI API. | ||
| 28 | + | ||
| 29 | + https://git.rickub.com/bots-garden/mini-me | ||
| 30 | + Docs: docs/en (English), docs/fr (français) · mm -h for the flags | ||
| 31 | + ``` | ||
| 32 | + | ||
| 33 | + `internal/version/about.go`, next to `Short()` and `Long()`: it is the same question asked with more room — "what am I running?" — and a binary that can answer it needs no network and no file to do so. The version shown is the one `--version` reports, by construction. | ||
| 34 | + | ||
| 35 | + Answered in `main.go` right after `flag.Parse()`, **before the configuration is read**, exactly as `--version` is: asking what `mm` is must work in any directory, with no config file and no model server running. | ||
| 36 | + | ||
| 37 | + Deliberately one screen. An `--about` that scrolls is a README in the wrong place, and the README is one link away — hence the last line rather than a copy of it. | ||
| 38 | + | ||
| 39 | + Tests: `internal/version/about_test.go` — the name, the version, the project URL and each front end are present; it stays under twenty lines; it does not end with a newline (the caller's `Println` adds one); and it still reports the right version on an unstamped build, which is what anyone building from source gets. | ||
| 40 | + | ||
| 41 | + Docs EN+FR: `reference/cli.md` (invocation line, flag table, the Version section) and the README install block. | ||
| 42 | + | ||
| 43 | + Not done: no `/about` slash command. The ticket says flag, and the three terminal commands that exist (`/help`, `/quit`, `/abort`) are about the session, not the program. Say so if you want one — it is a row in `internal/command`. | ||
| 44 | +tasks: | ||
| 45 | + - id: 1 | ||
| 46 | + title: "--about prints what mm is, before the configuration is read" | ||
| 47 | + state: closed | ||
| 48 | + priority: none | ||
| 49 | + author: | ||
| 50 | + name: k33g | ||
| 51 | + email: ph.charriere@gmail.com | ||
| 52 | + createdAt: 2026-09-21T05:00:00.000Z | ||
| 53 | + updatedAt: 2026-09-21T05:20:00.000Z | ||
| 54 | + closedAt: 2026-09-21T05:20:00.000Z | ||
| 55 | + - id: 2 | ||
| 56 | + title: "Tests and EN+FR docs" | ||
| 57 | + state: closed | ||
| 58 | + priority: none | ||
| 59 | + author: | ||
| 60 | + name: k33g | ||
| 61 | + email: ph.charriere@gmail.com | ||
| 62 | + createdAt: 2026-09-21T05:00:00.000Z | ||
| 63 | + updatedAt: 2026-09-21T05:20:00.000Z | ||
| 64 | + closedAt: 2026-09-21T05:20:00.000Z | ||
| 14 | comments: [] | 65 | comments: [] |
modified
.tickets/issues/0004-do-we-need-mcp.yaml +104 -2 | @@ -6,10 +6,112 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-15T22:38:45.129Z |
| 9 | -updatedAt: 2026-09-18T18:45:14.523Z | |
| 9 | +updatedAt: 2026-09-21T05:45:00.000Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | milestone: v0.0.3 |
| 12 | 12 | body: | |
| 13 | 13 | I would say no - use a CLI |
| 14 | -tasks: [] | |
| 14 | + | |
| 15 | + --- | |
| 16 | + | |
| 17 | + ## Analysis (Claude, 2026-09-21 — not implemented) | |
| 18 | + | |
| 19 | + **Conclusion: the "no" holds — but the reason above is not the durable one, and the ticket can be closed on evidence for about half a day's work, without writing a line of MCP.** | |
| 20 | + | |
| 21 | + ### The fact that reframes the question | |
| 22 | + | |
| 23 | + `genkit/go v1.10.0` — already a direct dependency — **ships an MCP client**: `plugins/mcp/` (`client.go`, `host.go`, `tools.go`, and `server.go` for the reverse direction). `MCPHost.GetActiveTools(ctx, g)` returns `[]ai.Tool`, and `ai.Tool` satisfies `ai.ToolRef`. | |
| 24 | + | |
| 25 | + So the naive version is genuinely ~10 lines in `assembleTools` and it compiles. **That is a trap**, and it is the most important thing in this ticket. | |
| 26 | + | |
| 27 | + (It does pull `github.com/mark3labs/mcp-go` into a deliberately small dependency list — not in `go.sum` today.) | |
| 28 | + | |
| 29 | + ### Why a raw `GetActiveTools` splice must never ship | |
| 30 | + | |
| 31 | + The plugin's generated tool skips every contract `internal/tools` enforces: | |
| 32 | + | |
| 33 | + | Skipped | Consequence | | |
| 34 | + |---|---| | |
| 35 | + | the `ui.ActiveSink()` branch | **It never calls `Allow`.** In ACP and in the window, third-party code would run **un-gated** while mm's own `bash` asks for permission. Under the default `permissions: auto` that is worse still. | | |
| 36 | + | `record()` | the loop detector never sees MCP calls; a model looping on a remote tool loops until `maxTurns` | | |
| 37 | + | `truncate()` | a server returning 400 KB of JSON drops it whole into a 12B context, `maxOutput` ignored | | |
| 38 | + | the error contract | missing argument and transport failure come back as **Go errors**. mm returns failures as TEXT and reserves a Go error for `detector.Stuck`. An MCP typo would end the turn instead of teaching the model. | | |
| 39 | + | text extraction / `echoOutput` | returns a raw `*mcp.CallToolResult`; in the REPL an MCP call would print nothing at all | | |
| 40 | + | |
| 41 | + **If MCP ever lands here, it lands as wrapped tools** — one `genkit.DefineTool` per discovered tool, body following the `tools.LSP` / `lspEvented` skeleton (sink branch → ToolStart → Allow → ToolRunning → run → ToolEnd → `record(d, name, title, truncate(out))`). About 60 lines in `internal/tools/mcp.go` plus a thin `internal/mcp`. | |
| 42 | + | |
| 43 | + **Name collisions are two distinct failures**, both fatal: `genkit.DefineTool` **panics** on a duplicate registry key (a server exposing `bash` crashes mm at start-up), and `resolveUniqueTools` fails generation with `duplicate tool "x"` — every turn. So: prefix with the server name, then reject anything colliding with the six built-ins and warn, the way a missing LSP binary is skipped. | |
| 44 | + | |
| 45 | + **Permissions for a tool you did not write**: `ui.ToolEvent.Kind` decides gating, and MCP does not tell you the kind — only optional server-authored `readOnlyHint`/`destructiveHint` annotations, i.e. a claim by the party you have not audited. Classify every MCP tool as `execute` and gate it, with a per-server `permissions: ask` override. | |
| 46 | + | |
| 47 | + ### What carries over from `internal/lsp`, and what does not | |
| 48 | + | |
| 49 | + Nearly free: the config shape and its validation; "skip with a warning, never refuse to start"; `launchProcess` with `tail` on stderr (MCP stdio is the same JSON-RPC-2.0-on-pipes shape); one client per process, closed at exit. Note `main.go`'s `set.close = client.Close` is a plain assignment — a second closer means chaining it. | |
| 50 | + | |
| 51 | + Differs, in order of how much it hurts: | |
| 52 | + | |
| 53 | + 1. **Not lazy.** `internal/lsp` starts a server on first use and its tool description is static config text. MCP's tool list only exists after connecting, so start-up must connect and `tools/list` every server, blocking, with a timeout and a degrade path, **before the banner**. | |
| 54 | + 2. **Prompt cost, every single request.** 20 MCP tools sit beside mm's 6 in every call, forever. | |
| 55 | + 3. **Schemas at runtime** → no Go struct with `jsonschema_description` tags. You lose per-field coaxing *and* the input normalisation that makes the detector treat twenty mangled variants as one call. | |
| 56 | + 4. **HTTP servers** have no `exec.LookPath` analogue — availability becomes a network probe — and raise auth/secrets, for which mm has only `apiKeyEnv`. | |
| 57 | + 5. A stdio server dying mid-session: `internal/lsp` restarts it; here the tool list was baked into the prompt at start-up. | |
| 58 | + | |
| 59 | + ### The honest case FOR "use a CLI" | |
| 60 | + | |
| 61 | + Strongest point first, and it is not simplicity: | |
| 62 | + | |
| 63 | + - **`<cli> --help` is dynamic discovery that costs nothing until used.** The system prompt already instructs exactly that. MCP pays for discovery in every request forever. **For mm's target model class this is the whole ballgame** — `normalize`, `cleanSymbol` and `renameNeedsName` exist in `internal/tools/lsp.go` because mellum2-12b already mangles a six-field schema. | |
| 64 | + - **Composition**: pipes, `jq`, `&&` let the model shrink a 400 KB answer to five lines *before* it costs context. An MCP tool returns what it returns. | |
| 65 | + - Everything already built applies for free: truncate, the 30 s budget, cancellation through the turn context, the temp-file fix that stops a background job hanging the turn (ticket 26), loop detection on (command, output), the echo, one permission dialog users already understand. | |
| 66 | + - **`skills/` + `read_skill` is the MCP-description equivalent**, loaded on demand, in the repo, editable without Go. Server-authored tool descriptions are neither. | |
| 67 | + - Auth lives in the CLI (`gh auth`, `docker login`), not in mm. | |
| 68 | + - `internal/acp/acp.go` already states the position in code: *"bob's whole point is the opposite — one built-in tool."* | |
| 69 | + | |
| 70 | + What a CLI genuinely does not give: a validated argument schema (the model composes a command line as free text, and `maxTurns: 40` pays for the `--help` round trips), and **per-tool permission granularity** — `allow_always` on `bash` grants `rm -rf` forever, where MCP's grant is per tool. That second one is real, but it can be closed more cheaply with a command allowlist on `bash` than by adding MCP. | |
| 71 | + | |
| 72 | + ### Where a CLI is genuinely not enough | |
| 73 | + | |
| 74 | + Only two, and both are narrow: | |
| 75 | + | |
| 76 | + - **(a) Remote-only capability** — a hosted endpoint over streamable HTTP with OAuth and no CLI. `bash` + `curl` means the model hand-writes JSON-RPC envelopes: a bad client reimplemented in the prompt. | |
| 77 | + - **(b) `bashTool: false`** — a supported configuration in which the CLI route does not exist at all. "Capability without arbitrary execution" is a coherent product; today that mm can only read and edit files. | |
| 78 | + | |
| 79 | + (Granular permissions, cross-call state such as a live browser page or an open DB transaction, and interactivity — MCP elicitation, for which mm has no UI — are real but each has a cheaper answer.) | |
| 80 | + | |
| 81 | + ### The cheapest experiment — half a day, zero MCP code | |
| 82 | + | |
| 83 | + Measure the **cost** side first, since that is what the recommendation rests on. With `agent.lsp.yaml`'s model and prompt, add N throwaway tools carrying realistic MCP-sized schemas (a `genkit.DefineTool` + `ai.WithInputSchema` loop, ~15 lines, deleted afterwards) so the tool list goes 6 → 16 → 26. Re-run the existing prompts from `tests/` and `demo/` and count: does the model still pick `lsp` over grep, does `edit_file` still land, how many turns, and what does `LastInputTokens()` say the prompt now costs. | |
| 84 | + | |
| 85 | + **If a 12B model degrades at 16 tools, this ticket closes on evidence.** Only if it survives is it worth thirty minutes on a throwaway branch splicing `mcp.NewMCPHost` + `GetActiveTools` against one real server — not to ship, but to feel the permission, detector and truncate holes first-hand. | |
| 86 | + | |
| 87 | + ### Adjacent | |
| 88 | + | |
| 89 | + Tickets 32 (`--prompt`) and 33 (invoke another mini-me) are mm *becoming* the CLI — the same bet as this ticket. For mm being **invoked**, the protocol question is ACP, not MCP-client, which is the instinct already written in 33. | |
| 90 | + | |
| 91 | + `plugins/mcp` also has a `server.go`: **mm exposing its own tools over MCP** is a different and cheaper question, and deserves its own ticket rather than being folded into this one. | |
| 92 | +tasks: | |
| 93 | + - id: 1 | |
| 94 | + title: "Run the tool-count experiment (6 → 16 → 26 dummy tools) and record where the model degrades" | |
| 95 | + state: open | |
| 96 | + priority: medium | |
| 97 | + author: | |
| 98 | + name: k33g | |
| 99 | + email: ph.charriere@gmail.com | |
| 100 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 101 | + - id: 2 | |
| 102 | + title: "Close this ticket on that evidence, recording the falsifiable reason rather than 'MCP is bloat'" | |
| 103 | + state: open | |
| 104 | + priority: medium | |
| 105 | + author: | |
| 106 | + name: k33g | |
| 107 | + email: ph.charriere@gmail.com | |
| 108 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 109 | + - id: 3 | |
| 110 | + title: "Open a separate ticket for the reverse direction: mm exposing its tools over MCP (plugins/mcp server.go)" | |
| 111 | + state: open | |
| 112 | + priority: low | |
| 113 | + author: | |
| 114 | + name: k33g | |
| 115 | + email: ph.charriere@gmail.com | |
| 116 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 15 | 117 | comments: [] |
| @@ -6,10 +6,112 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-15T22:38:45.129Z | 8 | createdAt: 2026-09-15T22:38:45.129Z |
| 9 | -updatedAt: 2026-09-18T18:45:14.523Z | 9 | +updatedAt: 2026-09-21T05:45:00.000Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | milestone: v0.0.3 | 11 | milestone: v0.0.3 |
| 12 | body: | | 12 | body: | |
| 13 | I would say no - use a CLI | 13 | I would say no - use a CLI |
| 14 | -tasks: [] | 14 | + |
| 15 | + --- | ||
| 16 | + | ||
| 17 | + ## Analysis (Claude, 2026-09-21 — not implemented) | ||
| 18 | + | ||
| 19 | + **Conclusion: the "no" holds — but the reason above is not the durable one, and the ticket can be closed on evidence for about half a day's work, without writing a line of MCP.** | ||
| 20 | + | ||
| 21 | + ### The fact that reframes the question | ||
| 22 | + | ||
| 23 | + `genkit/go v1.10.0` — already a direct dependency — **ships an MCP client**: `plugins/mcp/` (`client.go`, `host.go`, `tools.go`, and `server.go` for the reverse direction). `MCPHost.GetActiveTools(ctx, g)` returns `[]ai.Tool`, and `ai.Tool` satisfies `ai.ToolRef`. | ||
| 24 | + | ||
| 25 | + So the naive version is genuinely ~10 lines in `assembleTools` and it compiles. **That is a trap**, and it is the most important thing in this ticket. | ||
| 26 | + | ||
| 27 | + (It does pull `github.com/mark3labs/mcp-go` into a deliberately small dependency list — not in `go.sum` today.) | ||
| 28 | + | ||
| 29 | + ### Why a raw `GetActiveTools` splice must never ship | ||
| 30 | + | ||
| 31 | + The plugin's generated tool skips every contract `internal/tools` enforces: | ||
| 32 | + | ||
| 33 | + | Skipped | Consequence | | ||
| 34 | + |---|---| | ||
| 35 | + | the `ui.ActiveSink()` branch | **It never calls `Allow`.** In ACP and in the window, third-party code would run **un-gated** while mm's own `bash` asks for permission. Under the default `permissions: auto` that is worse still. | | ||
| 36 | + | `record()` | the loop detector never sees MCP calls; a model looping on a remote tool loops until `maxTurns` | | ||
| 37 | + | `truncate()` | a server returning 400 KB of JSON drops it whole into a 12B context, `maxOutput` ignored | | ||
| 38 | + | the error contract | missing argument and transport failure come back as **Go errors**. mm returns failures as TEXT and reserves a Go error for `detector.Stuck`. An MCP typo would end the turn instead of teaching the model. | | ||
| 39 | + | text extraction / `echoOutput` | returns a raw `*mcp.CallToolResult`; in the REPL an MCP call would print nothing at all | | ||
| 40 | + | ||
| 41 | + **If MCP ever lands here, it lands as wrapped tools** — one `genkit.DefineTool` per discovered tool, body following the `tools.LSP` / `lspEvented` skeleton (sink branch → ToolStart → Allow → ToolRunning → run → ToolEnd → `record(d, name, title, truncate(out))`). About 60 lines in `internal/tools/mcp.go` plus a thin `internal/mcp`. | ||
| 42 | + | ||
| 43 | + **Name collisions are two distinct failures**, both fatal: `genkit.DefineTool` **panics** on a duplicate registry key (a server exposing `bash` crashes mm at start-up), and `resolveUniqueTools` fails generation with `duplicate tool "x"` — every turn. So: prefix with the server name, then reject anything colliding with the six built-ins and warn, the way a missing LSP binary is skipped. | ||
| 44 | + | ||
| 45 | + **Permissions for a tool you did not write**: `ui.ToolEvent.Kind` decides gating, and MCP does not tell you the kind — only optional server-authored `readOnlyHint`/`destructiveHint` annotations, i.e. a claim by the party you have not audited. Classify every MCP tool as `execute` and gate it, with a per-server `permissions: ask` override. | ||
| 46 | + | ||
| 47 | + ### What carries over from `internal/lsp`, and what does not | ||
| 48 | + | ||
| 49 | + Nearly free: the config shape and its validation; "skip with a warning, never refuse to start"; `launchProcess` with `tail` on stderr (MCP stdio is the same JSON-RPC-2.0-on-pipes shape); one client per process, closed at exit. Note `main.go`'s `set.close = client.Close` is a plain assignment — a second closer means chaining it. | ||
| 50 | + | ||
| 51 | + Differs, in order of how much it hurts: | ||
| 52 | + | ||
| 53 | + 1. **Not lazy.** `internal/lsp` starts a server on first use and its tool description is static config text. MCP's tool list only exists after connecting, so start-up must connect and `tools/list` every server, blocking, with a timeout and a degrade path, **before the banner**. | ||
| 54 | + 2. **Prompt cost, every single request.** 20 MCP tools sit beside mm's 6 in every call, forever. | ||
| 55 | + 3. **Schemas at runtime** → no Go struct with `jsonschema_description` tags. You lose per-field coaxing *and* the input normalisation that makes the detector treat twenty mangled variants as one call. | ||
| 56 | + 4. **HTTP servers** have no `exec.LookPath` analogue — availability becomes a network probe — and raise auth/secrets, for which mm has only `apiKeyEnv`. | ||
| 57 | + 5. A stdio server dying mid-session: `internal/lsp` restarts it; here the tool list was baked into the prompt at start-up. | ||
| 58 | + | ||
| 59 | + ### The honest case FOR "use a CLI" | ||
| 60 | + | ||
| 61 | + Strongest point first, and it is not simplicity: | ||
| 62 | + | ||
| 63 | + - **`<cli> --help` is dynamic discovery that costs nothing until used.** The system prompt already instructs exactly that. MCP pays for discovery in every request forever. **For mm's target model class this is the whole ballgame** — `normalize`, `cleanSymbol` and `renameNeedsName` exist in `internal/tools/lsp.go` because mellum2-12b already mangles a six-field schema. | ||
| 64 | + - **Composition**: pipes, `jq`, `&&` let the model shrink a 400 KB answer to five lines *before* it costs context. An MCP tool returns what it returns. | ||
| 65 | + - Everything already built applies for free: truncate, the 30 s budget, cancellation through the turn context, the temp-file fix that stops a background job hanging the turn (ticket 26), loop detection on (command, output), the echo, one permission dialog users already understand. | ||
| 66 | + - **`skills/` + `read_skill` is the MCP-description equivalent**, loaded on demand, in the repo, editable without Go. Server-authored tool descriptions are neither. | ||
| 67 | + - Auth lives in the CLI (`gh auth`, `docker login`), not in mm. | ||
| 68 | + - `internal/acp/acp.go` already states the position in code: *"bob's whole point is the opposite — one built-in tool."* | ||
| 69 | + | ||
| 70 | + What a CLI genuinely does not give: a validated argument schema (the model composes a command line as free text, and `maxTurns: 40` pays for the `--help` round trips), and **per-tool permission granularity** — `allow_always` on `bash` grants `rm -rf` forever, where MCP's grant is per tool. That second one is real, but it can be closed more cheaply with a command allowlist on `bash` than by adding MCP. | ||
| 71 | + | ||
| 72 | + ### Where a CLI is genuinely not enough | ||
| 73 | + | ||
| 74 | + Only two, and both are narrow: | ||
| 75 | + | ||
| 76 | + - **(a) Remote-only capability** — a hosted endpoint over streamable HTTP with OAuth and no CLI. `bash` + `curl` means the model hand-writes JSON-RPC envelopes: a bad client reimplemented in the prompt. | ||
| 77 | + - **(b) `bashTool: false`** — a supported configuration in which the CLI route does not exist at all. "Capability without arbitrary execution" is a coherent product; today that mm can only read and edit files. | ||
| 78 | + | ||
| 79 | + (Granular permissions, cross-call state such as a live browser page or an open DB transaction, and interactivity — MCP elicitation, for which mm has no UI — are real but each has a cheaper answer.) | ||
| 80 | + | ||
| 81 | + ### The cheapest experiment — half a day, zero MCP code | ||
| 82 | + | ||
| 83 | + Measure the **cost** side first, since that is what the recommendation rests on. With `agent.lsp.yaml`'s model and prompt, add N throwaway tools carrying realistic MCP-sized schemas (a `genkit.DefineTool` + `ai.WithInputSchema` loop, ~15 lines, deleted afterwards) so the tool list goes 6 → 16 → 26. Re-run the existing prompts from `tests/` and `demo/` and count: does the model still pick `lsp` over grep, does `edit_file` still land, how many turns, and what does `LastInputTokens()` say the prompt now costs. | ||
| 84 | + | ||
| 85 | + **If a 12B model degrades at 16 tools, this ticket closes on evidence.** Only if it survives is it worth thirty minutes on a throwaway branch splicing `mcp.NewMCPHost` + `GetActiveTools` against one real server — not to ship, but to feel the permission, detector and truncate holes first-hand. | ||
| 86 | + | ||
| 87 | + ### Adjacent | ||
| 88 | + | ||
| 89 | + Tickets 32 (`--prompt`) and 33 (invoke another mini-me) are mm *becoming* the CLI — the same bet as this ticket. For mm being **invoked**, the protocol question is ACP, not MCP-client, which is the instinct already written in 33. | ||
| 90 | + | ||
| 91 | + `plugins/mcp` also has a `server.go`: **mm exposing its own tools over MCP** is a different and cheaper question, and deserves its own ticket rather than being folded into this one. | ||
| 92 | +tasks: | ||
| 93 | + - id: 1 | ||
| 94 | + title: "Run the tool-count experiment (6 → 16 → 26 dummy tools) and record where the model degrades" | ||
| 95 | + state: open | ||
| 96 | + priority: medium | ||
| 97 | + author: | ||
| 98 | + name: k33g | ||
| 99 | + email: ph.charriere@gmail.com | ||
| 100 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 101 | + - id: 2 | ||
| 102 | + title: "Close this ticket on that evidence, recording the falsifiable reason rather than 'MCP is bloat'" | ||
| 103 | + state: open | ||
| 104 | + priority: medium | ||
| 105 | + author: | ||
| 106 | + name: k33g | ||
| 107 | + email: ph.charriere@gmail.com | ||
| 108 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 109 | + - id: 3 | ||
| 110 | + title: "Open a separate ticket for the reverse direction: mm exposing its tools over MCP (plugins/mcp server.go)" | ||
| 111 | + state: open | ||
| 112 | + priority: low | ||
| 113 | + author: | ||
| 114 | + name: k33g | ||
| 115 | + email: ph.charriere@gmail.com | ||
| 116 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 15 | comments: [] | 117 | comments: [] |
modified
.tickets/issues/0015-improve-skills-detection.yaml +120 -3 | @@ -6,9 +6,126 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-16T05:52:51.530Z |
| 9 | -updatedAt: 2026-09-18T18:46:02.559Z | |
| 9 | +updatedAt: 2026-09-21T05:45:00.000Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | milestone: v0.0.3 |
| 12 | -body: "" | |
| 13 | -tasks: [] | |
| 12 | +body: | | |
| 13 | + ## Analysis (Claude, 2026-09-21 — not implemented) | |
| 14 | + | |
| 15 | + ### "Detection" means three different things | |
| 16 | + | |
| 17 | + | Reading | State | | |
| 18 | + |---|---| | |
| 19 | + | **(a) discovery on disk** — which files count as a skill | mostly solid; two real defects remain, and neither is what the title probably means | | |
| 20 | + | **(b) the model detecting that a skill APPLIES** | **this is the broken one**, and the repository currently ships the signal for it and throws it away | | |
| 21 | + | **(c) diagnosing a miss** — seeing that a skill should have fired and did not | essentially unsupported, and it is the prerequisite for knowing whether any fix to (b) worked | | |
| 22 | + | |
| 23 | + ### Three concrete defects | |
| 24 | + | |
| 25 | + **1. The catalogue advertises a name `read_skill` cannot resolve.** `skills.Catalogue` names a skill by its front-matter `name`; `skills.Read` looks it up **by path**. When they differ, the tool fails on the very string the catalogue told the model to use, with a message that contradicts itself: | |
| 26 | + | |
| 27 | + ``` | |
| 28 | + No skill named "renamed". Available skills: renamed | |
| 29 | + ``` | |
| 30 | + | |
| 31 | + The loop detector then flags and hard-stops the turn after six attempts. `docs/{en,fr}/reference/skill-format.md` documents `name` as "shown in the catalogue **and accepted by `read_skill`**" — which is false. The shipped skills all have `name:` equal to the directory name, so it does not bite today; it bites the first user who renames one. | |
| 32 | + | |
| 33 | + **2. `keywords:` is written, documented, and discarded.** Every shipped skill carries a rich bilingual `keywords:` line (`greetings`: `greet, hello, hi, bonjour, coucou, saluer`; `lsp-navigation`: 17 terms). `parseHeader` reads exactly two keys, `name` and `description`, and `skill-format.md` states plainly that *"the `keywords` key is accepted in the file and ignored by the loader"*. **The lexical bridge from "salue Bob" or "qui appelle cette fonction ?" to a skill name exists on disk and never reaches the model.** For a 2B–12B model that is the whole of reading (b). | |
| 34 | + | |
| 35 | + **3. `bg-jobs` is a skill that does not exist.** All three shipped YAMLs *and* the built-in default prompt in `internal/config/config.go` instruct: *"Read the `bg-jobs` skill before you wait on, inspect or stop such a job."* `skills/` contains only `greetings`, `lsp-navigation`, `vulcan-salute`, `what-time-is-it`. **Every background-job request drives the model into a guaranteed-failing `read_skill` call**, and nothing detects the mismatch — not a test, not the banner. *(Verified independently, twice.)* | |
| 36 | + | |
| 37 | + Related: the SKILLS prompt block scopes skills to *"any request to DO something to a **Go project**"*, while three of the four shipped skills are greetings, a Vulcan salute and a clock. The instruction actively misroutes. | |
| 38 | + | |
| 39 | + ### And no way to see a miss | |
| 40 | + | |
| 41 | + - A BOM, a leading blank line, a folded `description: >`, or a typo (`descripton:`) yields a skill in the catalogue with a bare name and **no description** — the worst possible entry. Nothing warns: the banner only fires when the count is **zero**. | |
| 42 | + - The `· 📖 N skill(s)` recap is **omitted at zero**, unlike `⚙ N command(s)` which always prints. So there is no signal at all distinguishing "no skill applied" from "a skill applied and the model ignored it". | |
| 43 | + - `engine.Skills` counts `read_skill` **responses**, failures included: `📖 2 skill(s)` can mean two `No skill named …`. | |
| 44 | + - The session `errors` trail — the artefact meant for *"a coding agent asked why the run went wrong"* — has three categories (`generation`, `compaction`, `resume`) and never records a failed `read_skill`. | |
| 45 | + | |
| 46 | + ### Tier 1 — cheap and certain, do these regardless | |
| 47 | + | |
| 48 | + 1. **Resolve `read_skill` through the catalogue, not the path.** A `map[name]path` built in `List`, keeping the `filepath.Base` barrier. ~15 lines + 2 tests. One decision: duplicate `name:` across two files — first wins, plus a banner warning. | |
| 49 | + 2. **Ship `skills/bg-jobs/SKILL.md`, or delete the sentence from all four prompts.** Minutes. **The durable fix is the test**: assert that every skill name quoted in a shipped YAML and in the built-in default prompt actually resolves. ~20 lines, and it prevents the whole class. | |
| 50 | + 3. **Parse `keywords:` and render it in the catalogue.** One case in `parseHeader`, one format change in `Catalogue`. The data is already written everywhere. Cap keywords per skill (say 8) so a 30-skill catalogue does not blow up, and correct `skill-format.md`. It cannot make matching worse, and it retires a documented inconsistency either way. | |
| 51 | + 4. **Warn on a description-less skill**, not just on zero skills: `N skill(s) with no description in <dir> (<names>) — the model will pick blindly`. | |
| 52 | + 5. **Print `📖` unconditionally**, like `⚙`, and count only *successful* loads. `📖 0 skill(s)` on a turn where a skill should have fired is exactly the observation this ticket needs and cannot make today. Touches three recap sites — keep them consistent; print it only when `read_skill` is declared. | |
| 53 | + | |
| 54 | + ### Tier 2 — the actual (b) work, and it needs measurement first | |
| 55 | + | |
| 56 | + 6. **Build a skill-routing eval before touching the prompt.** ~20 `prompt → expected skill (or none)` pairs, run against the configured local model, reporting hit / miss / false positive. At `temperature: 0.0` a small model is reproducible, so the numbers mean something. **Items 7–9 are guesses until this exists.** If only one Tier-2 item gets done, do this one. Ticket 32's `--prompt` may be the cheapest driver. | |
| 57 | + 7. **Fix the SKILLS prompt block**: drop "Go project"; state it as *"before acting, check the `read_skill` catalogue for a matching procedure; if one matches, load it first; if none matches, proceed normally"* — the no-match escape is missing today. Add the block to the built-in default prompt, which has none at all. Risk is real: prompt changes on a 12B are non-monotonic, and an over-strong "always call read_skill first" produces spurious calls. | |
| 58 | + 8. **Repeat the catalogue in the system prompt.** Today the model sees the skill list *only* through the tool schema. Small models often weight the system prompt more heavily. ~20 lines — but it doubles the catalogue's token cost and breaks the package's stated principle ("turn that directory into ONE STRING"). Treat as an A/B, not a default. | |
| 59 | + 9. **Richer entries — "when to use it"**, extracted from a section or a `when:` key. `lsp-navigation/SKILL.md` already has such a section. Changes the documented format. | |
| 60 | + | |
| 61 | + ### Tier 3 — speculative | |
| 62 | + | |
| 63 | + 10. **Record tool-level failures in the session `errors` trail** (a `FailTool` category). Turns `jq .errors <session>` into a real answer to "why didn't the skill fire?". Costs an architectural thread from `internal/tools` to the `Keeper`, which is `Keeper`-free by design. Only if (c) proves a recurring need. | |
| 64 | + 11. **Deterministic pre-routing** — match the message against `keywords` in Go and pre-load or nudge. Moves the decision out of the model; false positives pre-load the wrong procedure. The fallback if item 6 shows 3/7/8 are not enough. **Do not start here.** | |
| 65 | + 12. Deeper nesting (`skills/<group>/<name>/SKILL.md`) and hot reload. Genkit tool descriptions are fixed at `DefineTool` time, so hot reload means re-declaring the tool per turn. No demonstrated demand. **Rank last: neither is this ticket.** | |
| 66 | + | |
| 67 | + ### Worth one curl before any prompt work | |
| 68 | + | |
| 69 | + The whole design rests on the OpenAI-compatible server delivering the full tool description to the model, intact and untruncated. Nobody has checked. It is cheap, and it would rule out a whole class of explanation. | |
| 70 | + | |
| 71 | + ### Not re-opened | |
| 72 | + | |
| 73 | + Decisions in force per `.memory/summary.md`, none of which the above touches: both layouts accepted; the flat file wins when both exist; `skills/README.md` skipped; relative `skillsDir` resolved by `resolveSkillsDir`; banner warning at count zero. | |
| 74 | +tasks: | |
| 75 | + - id: 1 | |
| 76 | + title: "Resolve read_skill through the catalogue's name, not the file path" | |
| 77 | + state: open | |
| 78 | + priority: high | |
| 79 | + author: | |
| 80 | + name: k33g | |
| 81 | + email: ph.charriere@gmail.com | |
| 82 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 83 | + - id: 2 | |
| 84 | + title: "bg-jobs: ship the skill or delete the sentence from the 3 YAMLs and the default prompt" | |
| 85 | + state: open | |
| 86 | + priority: high | |
| 87 | + author: | |
| 88 | + name: k33g | |
| 89 | + email: ph.charriere@gmail.com | |
| 90 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 91 | + - id: 3 | |
| 92 | + title: "Test that every skill named in a shipped prompt actually resolves" | |
| 93 | + state: open | |
| 94 | + priority: high | |
| 95 | + author: | |
| 96 | + name: k33g | |
| 97 | + email: ph.charriere@gmail.com | |
| 98 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 99 | + - id: 4 | |
| 100 | + title: "Parse keywords and render them in the catalogue; correct skill-format.md" | |
| 101 | + state: open | |
| 102 | + priority: medium | |
| 103 | + author: | |
| 104 | + name: k33g | |
| 105 | + email: ph.charriere@gmail.com | |
| 106 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 107 | + - id: 5 | |
| 108 | + title: "Warn on a skill with no description; print the 📖 recap unconditionally and count only successes" | |
| 109 | + state: open | |
| 110 | + priority: medium | |
| 111 | + author: | |
| 112 | + name: k33g | |
| 113 | + email: ph.charriere@gmail.com | |
| 114 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 115 | + - id: 6 | |
| 116 | + title: "Build a skill-routing eval before any prompt change" | |
| 117 | + state: open | |
| 118 | + priority: medium | |
| 119 | + author: | |
| 120 | + name: k33g | |
| 121 | + email: ph.charriere@gmail.com | |
| 122 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 123 | + - id: 7 | |
| 124 | + title: "Then: fix the SKILLS prompt block and add it to the built-in default prompt" | |
| 125 | + state: open | |
| 126 | + priority: low | |
| 127 | + author: | |
| 128 | + name: k33g | |
| 129 | + email: ph.charriere@gmail.com | |
| 130 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 14 | 131 | comments: [] |
| @@ -6,9 +6,126 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-16T05:52:51.530Z | 8 | createdAt: 2026-09-16T05:52:51.530Z |
| 9 | -updatedAt: 2026-09-18T18:46:02.559Z | 9 | +updatedAt: 2026-09-21T05:45:00.000Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | milestone: v0.0.3 | 11 | milestone: v0.0.3 |
| 12 | -body: "" | 12 | +body: | |
| 13 | -tasks: [] | 13 | + ## Analysis (Claude, 2026-09-21 — not implemented) |
| 14 | + | ||
| 15 | + ### "Detection" means three different things | ||
| 16 | + | ||
| 17 | + | Reading | State | | ||
| 18 | + |---|---| | ||
| 19 | + | **(a) discovery on disk** — which files count as a skill | mostly solid; two real defects remain, and neither is what the title probably means | | ||
| 20 | + | **(b) the model detecting that a skill APPLIES** | **this is the broken one**, and the repository currently ships the signal for it and throws it away | | ||
| 21 | + | **(c) diagnosing a miss** — seeing that a skill should have fired and did not | essentially unsupported, and it is the prerequisite for knowing whether any fix to (b) worked | | ||
| 22 | + | ||
| 23 | + ### Three concrete defects | ||
| 24 | + | ||
| 25 | + **1. The catalogue advertises a name `read_skill` cannot resolve.** `skills.Catalogue` names a skill by its front-matter `name`; `skills.Read` looks it up **by path**. When they differ, the tool fails on the very string the catalogue told the model to use, with a message that contradicts itself: | ||
| 26 | + | ||
| 27 | + ``` | ||
| 28 | + No skill named "renamed". Available skills: renamed | ||
| 29 | + ``` | ||
| 30 | + | ||
| 31 | + The loop detector then flags and hard-stops the turn after six attempts. `docs/{en,fr}/reference/skill-format.md` documents `name` as "shown in the catalogue **and accepted by `read_skill`**" — which is false. The shipped skills all have `name:` equal to the directory name, so it does not bite today; it bites the first user who renames one. | ||
| 32 | + | ||
| 33 | + **2. `keywords:` is written, documented, and discarded.** Every shipped skill carries a rich bilingual `keywords:` line (`greetings`: `greet, hello, hi, bonjour, coucou, saluer`; `lsp-navigation`: 17 terms). `parseHeader` reads exactly two keys, `name` and `description`, and `skill-format.md` states plainly that *"the `keywords` key is accepted in the file and ignored by the loader"*. **The lexical bridge from "salue Bob" or "qui appelle cette fonction ?" to a skill name exists on disk and never reaches the model.** For a 2B–12B model that is the whole of reading (b). | ||
| 34 | + | ||
| 35 | + **3. `bg-jobs` is a skill that does not exist.** All three shipped YAMLs *and* the built-in default prompt in `internal/config/config.go` instruct: *"Read the `bg-jobs` skill before you wait on, inspect or stop such a job."* `skills/` contains only `greetings`, `lsp-navigation`, `vulcan-salute`, `what-time-is-it`. **Every background-job request drives the model into a guaranteed-failing `read_skill` call**, and nothing detects the mismatch — not a test, not the banner. *(Verified independently, twice.)* | ||
| 36 | + | ||
| 37 | + Related: the SKILLS prompt block scopes skills to *"any request to DO something to a **Go project**"*, while three of the four shipped skills are greetings, a Vulcan salute and a clock. The instruction actively misroutes. | ||
| 38 | + | ||
| 39 | + ### And no way to see a miss | ||
| 40 | + | ||
| 41 | + - A BOM, a leading blank line, a folded `description: >`, or a typo (`descripton:`) yields a skill in the catalogue with a bare name and **no description** — the worst possible entry. Nothing warns: the banner only fires when the count is **zero**. | ||
| 42 | + - The `· 📖 N skill(s)` recap is **omitted at zero**, unlike `⚙ N command(s)` which always prints. So there is no signal at all distinguishing "no skill applied" from "a skill applied and the model ignored it". | ||
| 43 | + - `engine.Skills` counts `read_skill` **responses**, failures included: `📖 2 skill(s)` can mean two `No skill named …`. | ||
| 44 | + - The session `errors` trail — the artefact meant for *"a coding agent asked why the run went wrong"* — has three categories (`generation`, `compaction`, `resume`) and never records a failed `read_skill`. | ||
| 45 | + | ||
| 46 | + ### Tier 1 — cheap and certain, do these regardless | ||
| 47 | + | ||
| 48 | + 1. **Resolve `read_skill` through the catalogue, not the path.** A `map[name]path` built in `List`, keeping the `filepath.Base` barrier. ~15 lines + 2 tests. One decision: duplicate `name:` across two files — first wins, plus a banner warning. | ||
| 49 | + 2. **Ship `skills/bg-jobs/SKILL.md`, or delete the sentence from all four prompts.** Minutes. **The durable fix is the test**: assert that every skill name quoted in a shipped YAML and in the built-in default prompt actually resolves. ~20 lines, and it prevents the whole class. | ||
| 50 | + 3. **Parse `keywords:` and render it in the catalogue.** One case in `parseHeader`, one format change in `Catalogue`. The data is already written everywhere. Cap keywords per skill (say 8) so a 30-skill catalogue does not blow up, and correct `skill-format.md`. It cannot make matching worse, and it retires a documented inconsistency either way. | ||
| 51 | + 4. **Warn on a description-less skill**, not just on zero skills: `N skill(s) with no description in <dir> (<names>) — the model will pick blindly`. | ||
| 52 | + 5. **Print `📖` unconditionally**, like `⚙`, and count only *successful* loads. `📖 0 skill(s)` on a turn where a skill should have fired is exactly the observation this ticket needs and cannot make today. Touches three recap sites — keep them consistent; print it only when `read_skill` is declared. | ||
| 53 | + | ||
| 54 | + ### Tier 2 — the actual (b) work, and it needs measurement first | ||
| 55 | + | ||
| 56 | + 6. **Build a skill-routing eval before touching the prompt.** ~20 `prompt → expected skill (or none)` pairs, run against the configured local model, reporting hit / miss / false positive. At `temperature: 0.0` a small model is reproducible, so the numbers mean something. **Items 7–9 are guesses until this exists.** If only one Tier-2 item gets done, do this one. Ticket 32's `--prompt` may be the cheapest driver. | ||
| 57 | + 7. **Fix the SKILLS prompt block**: drop "Go project"; state it as *"before acting, check the `read_skill` catalogue for a matching procedure; if one matches, load it first; if none matches, proceed normally"* — the no-match escape is missing today. Add the block to the built-in default prompt, which has none at all. Risk is real: prompt changes on a 12B are non-monotonic, and an over-strong "always call read_skill first" produces spurious calls. | ||
| 58 | + 8. **Repeat the catalogue in the system prompt.** Today the model sees the skill list *only* through the tool schema. Small models often weight the system prompt more heavily. ~20 lines — but it doubles the catalogue's token cost and breaks the package's stated principle ("turn that directory into ONE STRING"). Treat as an A/B, not a default. | ||
| 59 | + 9. **Richer entries — "when to use it"**, extracted from a section or a `when:` key. `lsp-navigation/SKILL.md` already has such a section. Changes the documented format. | ||
| 60 | + | ||
| 61 | + ### Tier 3 — speculative | ||
| 62 | + | ||
| 63 | + 10. **Record tool-level failures in the session `errors` trail** (a `FailTool` category). Turns `jq .errors <session>` into a real answer to "why didn't the skill fire?". Costs an architectural thread from `internal/tools` to the `Keeper`, which is `Keeper`-free by design. Only if (c) proves a recurring need. | ||
| 64 | + 11. **Deterministic pre-routing** — match the message against `keywords` in Go and pre-load or nudge. Moves the decision out of the model; false positives pre-load the wrong procedure. The fallback if item 6 shows 3/7/8 are not enough. **Do not start here.** | ||
| 65 | + 12. Deeper nesting (`skills/<group>/<name>/SKILL.md`) and hot reload. Genkit tool descriptions are fixed at `DefineTool` time, so hot reload means re-declaring the tool per turn. No demonstrated demand. **Rank last: neither is this ticket.** | ||
| 66 | + | ||
| 67 | + ### Worth one curl before any prompt work | ||
| 68 | + | ||
| 69 | + The whole design rests on the OpenAI-compatible server delivering the full tool description to the model, intact and untruncated. Nobody has checked. It is cheap, and it would rule out a whole class of explanation. | ||
| 70 | + | ||
| 71 | + ### Not re-opened | ||
| 72 | + | ||
| 73 | + Decisions in force per `.memory/summary.md`, none of which the above touches: both layouts accepted; the flat file wins when both exist; `skills/README.md` skipped; relative `skillsDir` resolved by `resolveSkillsDir`; banner warning at count zero. | ||
| 74 | +tasks: | ||
| 75 | + - id: 1 | ||
| 76 | + title: "Resolve read_skill through the catalogue's name, not the file path" | ||
| 77 | + state: open | ||
| 78 | + priority: high | ||
| 79 | + author: | ||
| 80 | + name: k33g | ||
| 81 | + email: ph.charriere@gmail.com | ||
| 82 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 83 | + - id: 2 | ||
| 84 | + title: "bg-jobs: ship the skill or delete the sentence from the 3 YAMLs and the default prompt" | ||
| 85 | + state: open | ||
| 86 | + priority: high | ||
| 87 | + author: | ||
| 88 | + name: k33g | ||
| 89 | + email: ph.charriere@gmail.com | ||
| 90 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 91 | + - id: 3 | ||
| 92 | + title: "Test that every skill named in a shipped prompt actually resolves" | ||
| 93 | + state: open | ||
| 94 | + priority: high | ||
| 95 | + author: | ||
| 96 | + name: k33g | ||
| 97 | + email: ph.charriere@gmail.com | ||
| 98 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 99 | + - id: 4 | ||
| 100 | + title: "Parse keywords and render them in the catalogue; correct skill-format.md" | ||
| 101 | + state: open | ||
| 102 | + priority: medium | ||
| 103 | + author: | ||
| 104 | + name: k33g | ||
| 105 | + email: ph.charriere@gmail.com | ||
| 106 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 107 | + - id: 5 | ||
| 108 | + title: "Warn on a skill with no description; print the 📖 recap unconditionally and count only successes" | ||
| 109 | + state: open | ||
| 110 | + priority: medium | ||
| 111 | + author: | ||
| 112 | + name: k33g | ||
| 113 | + email: ph.charriere@gmail.com | ||
| 114 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 115 | + - id: 6 | ||
| 116 | + title: "Build a skill-routing eval before any prompt change" | ||
| 117 | + state: open | ||
| 118 | + priority: medium | ||
| 119 | + author: | ||
| 120 | + name: k33g | ||
| 121 | + email: ph.charriere@gmail.com | ||
| 122 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 123 | + - id: 7 | ||
| 124 | + title: "Then: fix the SKILLS prompt block and add it to the built-in default prompt" | ||
| 125 | + state: open | ||
| 126 | + priority: low | ||
| 127 | + author: | ||
| 128 | + name: k33g | ||
| 129 | + email: ph.charriere@gmail.com | ||
| 130 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 14 | comments: [] | 131 | comments: [] |
added
.tickets/issues/0024-reduce-model-id-display-length-in-the-header.yaml +79 -0 | new file mode 100644 | ||
| @@ -0,0 +1,79 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 24 | |
| 3 | +title: Reduce Model ID display length in the header | |
| 4 | +state: closed | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-09-18T18:57:19.208Z | |
| 9 | +updatedAt: 2026-09-21T05:20:00.000Z | |
| 10 | +labels: [] | |
| 11 | +milestone: v0.0.3 | |
| 12 | +epic: Improve TUI | |
| 13 | +body: | | |
| 14 | + ## Implementation note (Claude, 2026-09-21) | |
| 15 | + | |
| 16 | + A model identifier is an address, not a name. A local registry hands out things like | |
| 17 | + | |
| 18 | + ``` | |
| 19 | + huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | |
| 20 | + ``` | |
| 21 | + | |
| 22 | + — seventy characters, of which maybe thirty say anything. The window's header is the one line on screen for the whole session, and everything beside the model (the provider, the context gauge, the session id) was being pushed off the right edge by it. | |
| 23 | + | |
| 24 | + `internal/tui/modelname.go`, `shortModel()`, applied in `runner.header()`: | |
| 25 | + | |
| 26 | + | Step | Why | | |
| 27 | + |---|---| | |
| 28 | + | Drop everything up to the last `/` | the registry host and the organisation are routing, not identity | | |
| 29 | + | Drop a trailing `-gguf`, or `-gguf-<quant>` | the file format is not information, and that form repeats the tag | | |
| 30 | + | Keep the tag after `:` whole | it is the part that says what is actually loaded, and what tells two otherwise identical models apart | | |
| 31 | + | Cap at 28 characters, ellipsis included | a header that is all model is not a header | | |
| 32 | + | |
| 33 | + ``` | |
| 34 | + huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M → qwen3.5-2b:Q4_K_M | |
| 35 | + …/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M → mellum2-12b-a2.5b-ins…:Q4_K_M | |
| 36 | + huggingface.co/prunaai/meta-llama-guard-2-8b-gguf-smashed:Q4_K_M → meta-llama-guard-2-8…:Q4_K_M | |
| 37 | + ai/smollm2 → smollm2 | |
| 38 | + gpt-4o → gpt-4o | |
| 39 | + ``` | |
| 40 | + | |
| 41 | + The third line is the case that makes the rule careful rather than clever: `-smashed` is a real variant marker (pruna's compressed weights), so `-gguf` is only dropped when it ends the name or is followed by something that looks like a quantisation (`q4_k_m`, `bf16`, `iq3_xxs`). A blunt "cut at -gguf" would have silently renamed that model to a different one. | |
| 42 | + | |
| 43 | + **The full identifier stays reachable**: only the header is shortened. The start-up banner prints the identifier exactly as configured, and the window keeps that banner as the conversation's first line — so what is running is always one scroll away, and `--version`-style questions are never answered by a truncated string. | |
| 44 | + | |
| 45 | + Tests: `internal/tui/modelname_test.go` — the five shapes above, plus a bound that holds for any input including a 500-character one, the tag surviving the cut, and `header()` itself losing the registry host while keeping the model, the provider and the context gauge. | |
| 46 | + | |
| 47 | + Docs EN+FR: `reference/cli.md`, window-mode table, a "Model name" row. | |
| 48 | +tasks: | |
| 49 | + - id: 1 | |
| 50 | + title: "Shorten the model identifier in the window header, keeping the tag" | |
| 51 | + state: closed | |
| 52 | + priority: none | |
| 53 | + author: | |
| 54 | + name: k33g | |
| 55 | + email: ph.charriere@gmail.com | |
| 56 | + createdAt: 2026-09-21T05:00:00.000Z | |
| 57 | + updatedAt: 2026-09-21T05:20:00.000Z | |
| 58 | + closedAt: 2026-09-21T05:20:00.000Z | |
| 59 | + - id: 2 | |
| 60 | + title: "Never mistake a variant marker for the format suffix" | |
| 61 | + state: closed | |
| 62 | + priority: none | |
| 63 | + author: | |
| 64 | + name: k33g | |
| 65 | + email: ph.charriere@gmail.com | |
| 66 | + createdAt: 2026-09-21T05:00:00.000Z | |
| 67 | + updatedAt: 2026-09-21T05:20:00.000Z | |
| 68 | + closedAt: 2026-09-21T05:20:00.000Z | |
| 69 | + - id: 3 | |
| 70 | + title: "Tests and EN+FR docs" | |
| 71 | + state: closed | |
| 72 | + priority: none | |
| 73 | + author: | |
| 74 | + name: k33g | |
| 75 | + email: ph.charriere@gmail.com | |
| 76 | + createdAt: 2026-09-21T05:00:00.000Z | |
| 77 | + updatedAt: 2026-09-21T05:20:00.000Z | |
| 78 | + closedAt: 2026-09-21T05:20:00.000Z | |
| 79 | +comments: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,79 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 24 | ||
| 3 | +title: Reduce Model ID display length in the header | ||
| 4 | +state: closed | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-09-18T18:57:19.208Z | ||
| 9 | +updatedAt: 2026-09-21T05:20:00.000Z | ||
| 10 | +labels: [] | ||
| 11 | +milestone: v0.0.3 | ||
| 12 | +epic: Improve TUI | ||
| 13 | +body: | | ||
| 14 | + ## Implementation note (Claude, 2026-09-21) | ||
| 15 | + | ||
| 16 | + A model identifier is an address, not a name. A local registry hands out things like | ||
| 17 | + | ||
| 18 | + ``` | ||
| 19 | + huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | ||
| 20 | + ``` | ||
| 21 | + | ||
| 22 | + — seventy characters, of which maybe thirty say anything. The window's header is the one line on screen for the whole session, and everything beside the model (the provider, the context gauge, the session id) was being pushed off the right edge by it. | ||
| 23 | + | ||
| 24 | + `internal/tui/modelname.go`, `shortModel()`, applied in `runner.header()`: | ||
| 25 | + | ||
| 26 | + | Step | Why | | ||
| 27 | + |---|---| | ||
| 28 | + | Drop everything up to the last `/` | the registry host and the organisation are routing, not identity | | ||
| 29 | + | Drop a trailing `-gguf`, or `-gguf-<quant>` | the file format is not information, and that form repeats the tag | | ||
| 30 | + | Keep the tag after `:` whole | it is the part that says what is actually loaded, and what tells two otherwise identical models apart | | ||
| 31 | + | Cap at 28 characters, ellipsis included | a header that is all model is not a header | | ||
| 32 | + | ||
| 33 | + ``` | ||
| 34 | + huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M → qwen3.5-2b:Q4_K_M | ||
| 35 | + …/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M → mellum2-12b-a2.5b-ins…:Q4_K_M | ||
| 36 | + huggingface.co/prunaai/meta-llama-guard-2-8b-gguf-smashed:Q4_K_M → meta-llama-guard-2-8…:Q4_K_M | ||
| 37 | + ai/smollm2 → smollm2 | ||
| 38 | + gpt-4o → gpt-4o | ||
| 39 | + ``` | ||
| 40 | + | ||
| 41 | + The third line is the case that makes the rule careful rather than clever: `-smashed` is a real variant marker (pruna's compressed weights), so `-gguf` is only dropped when it ends the name or is followed by something that looks like a quantisation (`q4_k_m`, `bf16`, `iq3_xxs`). A blunt "cut at -gguf" would have silently renamed that model to a different one. | ||
| 42 | + | ||
| 43 | + **The full identifier stays reachable**: only the header is shortened. The start-up banner prints the identifier exactly as configured, and the window keeps that banner as the conversation's first line — so what is running is always one scroll away, and `--version`-style questions are never answered by a truncated string. | ||
| 44 | + | ||
| 45 | + Tests: `internal/tui/modelname_test.go` — the five shapes above, plus a bound that holds for any input including a 500-character one, the tag surviving the cut, and `header()` itself losing the registry host while keeping the model, the provider and the context gauge. | ||
| 46 | + | ||
| 47 | + Docs EN+FR: `reference/cli.md`, window-mode table, a "Model name" row. | ||
| 48 | +tasks: | ||
| 49 | + - id: 1 | ||
| 50 | + title: "Shorten the model identifier in the window header, keeping the tag" | ||
| 51 | + state: closed | ||
| 52 | + priority: none | ||
| 53 | + author: | ||
| 54 | + name: k33g | ||
| 55 | + email: ph.charriere@gmail.com | ||
| 56 | + createdAt: 2026-09-21T05:00:00.000Z | ||
| 57 | + updatedAt: 2026-09-21T05:20:00.000Z | ||
| 58 | + closedAt: 2026-09-21T05:20:00.000Z | ||
| 59 | + - id: 2 | ||
| 60 | + title: "Never mistake a variant marker for the format suffix" | ||
| 61 | + state: closed | ||
| 62 | + priority: none | ||
| 63 | + author: | ||
| 64 | + name: k33g | ||
| 65 | + email: ph.charriere@gmail.com | ||
| 66 | + createdAt: 2026-09-21T05:00:00.000Z | ||
| 67 | + updatedAt: 2026-09-21T05:20:00.000Z | ||
| 68 | + closedAt: 2026-09-21T05:20:00.000Z | ||
| 69 | + - id: 3 | ||
| 70 | + title: "Tests and EN+FR docs" | ||
| 71 | + state: closed | ||
| 72 | + priority: none | ||
| 73 | + author: | ||
| 74 | + name: k33g | ||
| 75 | + email: ph.charriere@gmail.com | ||
| 76 | + createdAt: 2026-09-21T05:00:00.000Z | ||
| 77 | + updatedAt: 2026-09-21T05:20:00.000Z | ||
| 78 | + closedAt: 2026-09-21T05:20:00.000Z | ||
| 79 | +comments: [] | ||
deleted
.tickets/issues/0024-reduce-model-id-display-length.yaml +0 -15 | deleted file mode 100644 | ||
| @@ -1,15 +0,0 @@ | ||
| 1 | -# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | -id: 24 | |
| 3 | -title: Reduce Model ID display length | |
| 4 | -state: open | |
| 5 | -author: | |
| 6 | - name: k33g | |
| 7 | - email: ph.charriere@gmail.com | |
| 8 | -createdAt: 2026-09-18T18:57:19.208Z | |
| 9 | -updatedAt: 2026-09-18T18:58:05.672Z | |
| 10 | -labels: [] | |
| 11 | -milestone: v0.0.3 | |
| 12 | -epic: Improve TUI | |
| 13 | -body: "" | |
| 14 | -tasks: [] | |
| 15 | -comments: [] | |
| deleted file mode 100644 | |||
| @@ -1,15 +0,0 @@ | |||
| 1 | -# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | -id: 24 | ||
| 3 | -title: Reduce Model ID display length | ||
| 4 | -state: open | ||
| 5 | -author: | ||
| 6 | - name: k33g | ||
| 7 | - email: ph.charriere@gmail.com | ||
| 8 | -createdAt: 2026-09-18T18:57:19.208Z | ||
| 9 | -updatedAt: 2026-09-18T18:58:05.672Z | ||
| 10 | -labels: [] | ||
| 11 | -milestone: v0.0.3 | ||
| 12 | -epic: Improve TUI | ||
| 13 | -body: "" | ||
| 14 | -tasks: [] | ||
| 15 | -comments: [] | ||
modified
.tickets/issues/0030-how-to-use-mini-me-with-claude.yaml +113 -3 | @@ -6,9 +6,119 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T04:00:56.971Z |
| 9 | -updatedAt: 2026-09-21T04:00:56.971Z | |
| 9 | +updatedAt: 2026-09-21T05:45:00.000Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | milestone: v0.0.3 |
| 12 | -body: "" | |
| 13 | -tasks: [] | |
| 12 | +body: | | |
| 13 | + ## Analysis (Claude, 2026-09-21 — not implemented) | |
| 14 | + | |
| 15 | + **This ticket is not symmetric with 31. One experiment decides which of two very different routes it takes, and the experiment costs a `curl` and an API key. Run it before writing anything.** | |
| 16 | + | |
| 17 | + ### The experiment | |
| 18 | + | |
| 19 | + ``` | |
| 20 | + curl https://api.anthropic.com/v1/chat/completions \ | |
| 21 | + -H "Authorization: Bearer $ANTHROPIC_API_KEY" -H 'Content-Type: application/json' \ | |
| 22 | + -d '{"model":"<a claude id>","stream":true, | |
| 23 | + "tools":[{"type":"function","function":{"name":"bash","parameters":{...}}}], | |
| 24 | + "messages":[{"role":"user","content":"list the files here"}]}' | |
| 25 | + ``` | |
| 26 | + | |
| 27 | + **Does a tool call come back?** | |
| 28 | + | |
| 29 | + - **Yes** → the same ~20-line registry entry as ticket 31. Half a day. | |
| 30 | + - **No** → Claude earns a genuinely new `Provider` on Genkit's native `plugins/anthropic`. Two to three days, and it is the case `internal/engine/provider.go` already anticipates in its own words: *"adding a protocol (Anthropic's Messages API…) is a new type."* | |
| 31 | + | |
| 32 | + Why it is genuinely open: Anthropic's OpenAI-compatible endpoint **is live** (measured — `POST /v1/chat/completions` with no key returns an OpenAI-shaped `{"error":{"code":"invalid_request_error", …}}`), but Genkit's own `compat_oai/anthropic` plugin marks every model `Tools: false` with the comment *"Anthropic supports tool use, but it's not compatible with the OpenAI API"*. That assertion could not be checked without a key, and it dates from 2025. An agent that cannot call tools is not an agent, so this is the whole ticket. | |
| 33 | + | |
| 34 | + Note mm is **not** bound by that flag: `openaiCompat.Open` declares its own model with `compat_oai.Multimodal`. The flag is Genkit's claim about the wire, not a gate on us. | |
| 35 | + | |
| 36 | + ### What Genkit v1.10.0 actually ships | |
| 37 | + | |
| 38 | + | Plugin | Verdict | | |
| 39 | + |---|---| | |
| 40 | + | `plugins/compat_oai/anthropic` | **Useless here.** It reads `ANTHROPIC_API_KEY`/`ANTHROPIC_BASE_URL` from the environment itself, which fights `Backend`; it hard-codes eight model ids; and it marks `Tools: false`. `openaiCompat.Open` already does everything it does, better. | | |
| 41 | + | `plugins/anthropic` (native) | The real Messages API on `anthropic-sdk-go`. `Tools: true`, `ToolChoice`, `Constrained`. **Streams `ai.NewReasoningPart` on `thinking_delta` and round-trips the thinking signature.** `Init` panics when the key is unset. | | |
| 42 | + | |
| 43 | + Neither `anthropic-sdk-go` nor `google.golang.org/genai` is in the module cache today — the native route adds real dependencies. | |
| 44 | + | |
| 45 | + ### Route A — registry entry (if tools work) | |
| 46 | + | |
| 47 | + Same shape as ticket 31: `baseURL: "https://api.anthropic.com/v1"`, `apiKeyEnv: "ANTHROPIC_API_KEY"`, `keyRequired: true`, `fallback: ""`. Same caveats as 31, with one extra: | |
| 48 | + | |
| 49 | + - **`reachable()` breaks harder.** It sends a bare GET to `/models` and demands 200; Anthropic's `/v1/models` is the *native* route and returns 400 `"anthropic-version: header is required"` even with a Bearer token. Same `probeRemote` fix as ticket 31, plus the vendor header. | |
| 50 | + - **Reasoning is the real hazard.** `reasoningTap` (ticket 29) scans for `delta.reasoning_content` / `delta.reasoning`. Anthropic's compat layer is not known to emit either. Losing it loses the 💭 **and the watchdog heartbeat** — and `engine.go` spells out why that is not cosmetic: `beat()` lives in that listener, so a model silent longer than `watchdogTimeout` is killed as stalled while it is working. **Claude with extended thinking on is exactly that shape.** Mitigations: a much larger `watchdogTimeout` in the shipped YAML, or teaching `reasoningTap` a third spelling if one turns up. | |
| 51 | + | |
| 52 | + ### Route B — native provider (if tools do not work) | |
| 53 | + | |
| 54 | + A new `Provider` implementation, not a registry entry. What changes beyond the obvious: | |
| 55 | + | |
| 56 | + - **No middleware hook.** `option.WithMiddleware(reasoningTap)` is `openai-go/option`; `anthropic-sdk-go` has a same-named, different package. **You do not need it**: the native plugin already emits reasoning parts, so `beat()` fires through the ordinary streaming callback and you add `think.emit(chunk.Reasoning())` next to `emit(chunk.Text())`. Reasoning gets *simpler* — but `Generate` grows a second path and `WithReasoning` becomes provider-conditional. | |
| 57 | + - **`sampling:` stops being portable.** `config.Cfg.Sampling` goes raw into `ai.WithConfig`. The native plugin maps it onto `anthropic.MessageNewParams` and **hard-errors `"maxTokens not set"` when `max_tokens` is absent**. Either a per-provider dialect or a translation step in `Provider`. *(Unverified: the SDK is not in the module cache.)* | |
| 58 | + - `Init` **panics** on a missing key where `New`→`Open` returns errors. `keyRequired` catches the ordinary case first, but `Open` wants a guard. | |
| 59 | + - `Explain` is new work: `errors.As(err, &openai.Error)` never matches, and `statusInText` may not match the other SDK's error strings. No branch exists for Anthropic's 529 overloaded. | |
| 60 | + - What Route B buys that Route A never can: **extended thinking with signatures flowing straight into the 💭 block through `chunk.Reasoning()`, with the watchdog fed for free**, plus `ThoughtsTokens` in the usage. | |
| 61 | + | |
| 62 | + ### Cost | |
| 63 | + | |
| 64 | + | | Route A | Route B | | |
| 65 | + |---|---|---| | |
| 66 | + | Code | ~40 lines + ~20 for `probeRemote` | ~150–250 lines, new file | | |
| 67 | + | New dependencies | none | `anthropic-sdk-go`, not cached | | |
| 68 | + | Touches | `init()`, one probe func | `Generate`, `Explain`, sampling, `Probe`, `Open` | | |
| 69 | + | Effort | half a day | 2–3 days | | |
| 70 | + | Risk | concentrated in one yes/no question | diffuse: a second code path, forever | | |
| 71 | + | |
| 72 | + ### To decide before coding | |
| 73 | + | |
| 74 | + 1. **Which surface?** Claude's direct API, or Bedrock? One registry entry cannot serve both. | |
| 75 | + 2. **Is thinking in scope?** If "support Claude" means "extended thinking visible in the 💭 block", Route A almost certainly cannot deliver it and the answer flips before the experiment is even run. | |
| 76 | + 3. **Per-vendor entries, or one generic `openai` entry** where the user supplies `baseUrl`? That single entry would cover Claude, Gemini, OpenRouter, Groq, Together and vLLM at once, and is arguably the real ticket behind both 30 and 31. It trades good per-vendor `Explain` words for reach. | |
| 77 | + 4. Which model ids ship, and are they pinned? Hosted ids go stale in months. | |
| 78 | + 5. Metered-API guardrails: `maxTurns: 40` plus a `bash` tool is a different proposition when every turn is billed. mm already captures `Usage.InputTokens`. | |
| 79 | + | |
| 80 | + ### What I would resist | |
| 81 | + | |
| 82 | + Doing the native route for **both** vendors. Gemini through `googlegenai` costs a silent sampling mismatch and an extra dependency tree for a capability its OpenAI layer already provides (ticket 31). | |
| 83 | +tasks: | |
| 84 | + - id: 1 | |
| 85 | + title: "EXPERIMENT FIRST: does Anthropic's /v1/chat/completions return tool calls? One curl with a real key." | |
| 86 | + state: open | |
| 87 | + priority: high | |
| 88 | + author: | |
| 89 | + name: k33g | |
| 90 | + email: ph.charriere@gmail.com | |
| 91 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 92 | + - id: 2 | |
| 93 | + title: "Decide Route A (registry entry) or Route B (native plugins/anthropic) on that result" | |
| 94 | + state: open | |
| 95 | + priority: high | |
| 96 | + author: | |
| 97 | + name: k33g | |
| 98 | + email: ph.charriere@gmail.com | |
| 99 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 100 | + - id: 3 | |
| 101 | + title: "Check what happens to extended thinking: does reasoningTap see anything, and does the watchdog survive?" | |
| 102 | + state: open | |
| 103 | + priority: high | |
| 104 | + author: | |
| 105 | + name: k33g | |
| 106 | + email: ph.charriere@gmail.com | |
| 107 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 108 | + - id: 4 | |
| 109 | + title: "probeRemote with the anthropic-version header, so the banner stops crying wolf" | |
| 110 | + state: open | |
| 111 | + priority: medium | |
| 112 | + author: | |
| 113 | + name: k33g | |
| 114 | + email: ph.charriere@gmail.com | |
| 115 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 116 | + - id: 5 | |
| 117 | + title: "agent.claude.yaml and EN+FR how-to + providers/configuration docs" | |
| 118 | + state: open | |
| 119 | + priority: low | |
| 120 | + author: | |
| 121 | + name: k33g | |
| 122 | + email: ph.charriere@gmail.com | |
| 123 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 14 | 124 | comments: [] |
| @@ -6,9 +6,119 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T04:00:56.971Z | 8 | createdAt: 2026-09-21T04:00:56.971Z |
| 9 | -updatedAt: 2026-09-21T04:00:56.971Z | 9 | +updatedAt: 2026-09-21T05:45:00.000Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | milestone: v0.0.3 | 11 | milestone: v0.0.3 |
| 12 | -body: "" | 12 | +body: | |
| 13 | -tasks: [] | 13 | + ## Analysis (Claude, 2026-09-21 — not implemented) |
| 14 | + | ||
| 15 | + **This ticket is not symmetric with 31. One experiment decides which of two very different routes it takes, and the experiment costs a `curl` and an API key. Run it before writing anything.** | ||
| 16 | + | ||
| 17 | + ### The experiment | ||
| 18 | + | ||
| 19 | + ``` | ||
| 20 | + curl https://api.anthropic.com/v1/chat/completions \ | ||
| 21 | + -H "Authorization: Bearer $ANTHROPIC_API_KEY" -H 'Content-Type: application/json' \ | ||
| 22 | + -d '{"model":"<a claude id>","stream":true, | ||
| 23 | + "tools":[{"type":"function","function":{"name":"bash","parameters":{...}}}], | ||
| 24 | + "messages":[{"role":"user","content":"list the files here"}]}' | ||
| 25 | + ``` | ||
| 26 | + | ||
| 27 | + **Does a tool call come back?** | ||
| 28 | + | ||
| 29 | + - **Yes** → the same ~20-line registry entry as ticket 31. Half a day. | ||
| 30 | + - **No** → Claude earns a genuinely new `Provider` on Genkit's native `plugins/anthropic`. Two to three days, and it is the case `internal/engine/provider.go` already anticipates in its own words: *"adding a protocol (Anthropic's Messages API…) is a new type."* | ||
| 31 | + | ||
| 32 | + Why it is genuinely open: Anthropic's OpenAI-compatible endpoint **is live** (measured — `POST /v1/chat/completions` with no key returns an OpenAI-shaped `{"error":{"code":"invalid_request_error", …}}`), but Genkit's own `compat_oai/anthropic` plugin marks every model `Tools: false` with the comment *"Anthropic supports tool use, but it's not compatible with the OpenAI API"*. That assertion could not be checked without a key, and it dates from 2025. An agent that cannot call tools is not an agent, so this is the whole ticket. | ||
| 33 | + | ||
| 34 | + Note mm is **not** bound by that flag: `openaiCompat.Open` declares its own model with `compat_oai.Multimodal`. The flag is Genkit's claim about the wire, not a gate on us. | ||
| 35 | + | ||
| 36 | + ### What Genkit v1.10.0 actually ships | ||
| 37 | + | ||
| 38 | + | Plugin | Verdict | | ||
| 39 | + |---|---| | ||
| 40 | + | `plugins/compat_oai/anthropic` | **Useless here.** It reads `ANTHROPIC_API_KEY`/`ANTHROPIC_BASE_URL` from the environment itself, which fights `Backend`; it hard-codes eight model ids; and it marks `Tools: false`. `openaiCompat.Open` already does everything it does, better. | | ||
| 41 | + | `plugins/anthropic` (native) | The real Messages API on `anthropic-sdk-go`. `Tools: true`, `ToolChoice`, `Constrained`. **Streams `ai.NewReasoningPart` on `thinking_delta` and round-trips the thinking signature.** `Init` panics when the key is unset. | | ||
| 42 | + | ||
| 43 | + Neither `anthropic-sdk-go` nor `google.golang.org/genai` is in the module cache today — the native route adds real dependencies. | ||
| 44 | + | ||
| 45 | + ### Route A — registry entry (if tools work) | ||
| 46 | + | ||
| 47 | + Same shape as ticket 31: `baseURL: "https://api.anthropic.com/v1"`, `apiKeyEnv: "ANTHROPIC_API_KEY"`, `keyRequired: true`, `fallback: ""`. Same caveats as 31, with one extra: | ||
| 48 | + | ||
| 49 | + - **`reachable()` breaks harder.** It sends a bare GET to `/models` and demands 200; Anthropic's `/v1/models` is the *native* route and returns 400 `"anthropic-version: header is required"` even with a Bearer token. Same `probeRemote` fix as ticket 31, plus the vendor header. | ||
| 50 | + - **Reasoning is the real hazard.** `reasoningTap` (ticket 29) scans for `delta.reasoning_content` / `delta.reasoning`. Anthropic's compat layer is not known to emit either. Losing it loses the 💭 **and the watchdog heartbeat** — and `engine.go` spells out why that is not cosmetic: `beat()` lives in that listener, so a model silent longer than `watchdogTimeout` is killed as stalled while it is working. **Claude with extended thinking on is exactly that shape.** Mitigations: a much larger `watchdogTimeout` in the shipped YAML, or teaching `reasoningTap` a third spelling if one turns up. | ||
| 51 | + | ||
| 52 | + ### Route B — native provider (if tools do not work) | ||
| 53 | + | ||
| 54 | + A new `Provider` implementation, not a registry entry. What changes beyond the obvious: | ||
| 55 | + | ||
| 56 | + - **No middleware hook.** `option.WithMiddleware(reasoningTap)` is `openai-go/option`; `anthropic-sdk-go` has a same-named, different package. **You do not need it**: the native plugin already emits reasoning parts, so `beat()` fires through the ordinary streaming callback and you add `think.emit(chunk.Reasoning())` next to `emit(chunk.Text())`. Reasoning gets *simpler* — but `Generate` grows a second path and `WithReasoning` becomes provider-conditional. | ||
| 57 | + - **`sampling:` stops being portable.** `config.Cfg.Sampling` goes raw into `ai.WithConfig`. The native plugin maps it onto `anthropic.MessageNewParams` and **hard-errors `"maxTokens not set"` when `max_tokens` is absent**. Either a per-provider dialect or a translation step in `Provider`. *(Unverified: the SDK is not in the module cache.)* | ||
| 58 | + - `Init` **panics** on a missing key where `New`→`Open` returns errors. `keyRequired` catches the ordinary case first, but `Open` wants a guard. | ||
| 59 | + - `Explain` is new work: `errors.As(err, &openai.Error)` never matches, and `statusInText` may not match the other SDK's error strings. No branch exists for Anthropic's 529 overloaded. | ||
| 60 | + - What Route B buys that Route A never can: **extended thinking with signatures flowing straight into the 💭 block through `chunk.Reasoning()`, with the watchdog fed for free**, plus `ThoughtsTokens` in the usage. | ||
| 61 | + | ||
| 62 | + ### Cost | ||
| 63 | + | ||
| 64 | + | | Route A | Route B | | ||
| 65 | + |---|---|---| | ||
| 66 | + | Code | ~40 lines + ~20 for `probeRemote` | ~150–250 lines, new file | | ||
| 67 | + | New dependencies | none | `anthropic-sdk-go`, not cached | | ||
| 68 | + | Touches | `init()`, one probe func | `Generate`, `Explain`, sampling, `Probe`, `Open` | | ||
| 69 | + | Effort | half a day | 2–3 days | | ||
| 70 | + | Risk | concentrated in one yes/no question | diffuse: a second code path, forever | | ||
| 71 | + | ||
| 72 | + ### To decide before coding | ||
| 73 | + | ||
| 74 | + 1. **Which surface?** Claude's direct API, or Bedrock? One registry entry cannot serve both. | ||
| 75 | + 2. **Is thinking in scope?** If "support Claude" means "extended thinking visible in the 💭 block", Route A almost certainly cannot deliver it and the answer flips before the experiment is even run. | ||
| 76 | + 3. **Per-vendor entries, or one generic `openai` entry** where the user supplies `baseUrl`? That single entry would cover Claude, Gemini, OpenRouter, Groq, Together and vLLM at once, and is arguably the real ticket behind both 30 and 31. It trades good per-vendor `Explain` words for reach. | ||
| 77 | + 4. Which model ids ship, and are they pinned? Hosted ids go stale in months. | ||
| 78 | + 5. Metered-API guardrails: `maxTurns: 40` plus a `bash` tool is a different proposition when every turn is billed. mm already captures `Usage.InputTokens`. | ||
| 79 | + | ||
| 80 | + ### What I would resist | ||
| 81 | + | ||
| 82 | + Doing the native route for **both** vendors. Gemini through `googlegenai` costs a silent sampling mismatch and an extra dependency tree for a capability its OpenAI layer already provides (ticket 31). | ||
| 83 | +tasks: | ||
| 84 | + - id: 1 | ||
| 85 | + title: "EXPERIMENT FIRST: does Anthropic's /v1/chat/completions return tool calls? One curl with a real key." | ||
| 86 | + state: open | ||
| 87 | + priority: high | ||
| 88 | + author: | ||
| 89 | + name: k33g | ||
| 90 | + email: ph.charriere@gmail.com | ||
| 91 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 92 | + - id: 2 | ||
| 93 | + title: "Decide Route A (registry entry) or Route B (native plugins/anthropic) on that result" | ||
| 94 | + state: open | ||
| 95 | + priority: high | ||
| 96 | + author: | ||
| 97 | + name: k33g | ||
| 98 | + email: ph.charriere@gmail.com | ||
| 99 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 100 | + - id: 3 | ||
| 101 | + title: "Check what happens to extended thinking: does reasoningTap see anything, and does the watchdog survive?" | ||
| 102 | + state: open | ||
| 103 | + priority: high | ||
| 104 | + author: | ||
| 105 | + name: k33g | ||
| 106 | + email: ph.charriere@gmail.com | ||
| 107 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 108 | + - id: 4 | ||
| 109 | + title: "probeRemote with the anthropic-version header, so the banner stops crying wolf" | ||
| 110 | + state: open | ||
| 111 | + priority: medium | ||
| 112 | + author: | ||
| 113 | + name: k33g | ||
| 114 | + email: ph.charriere@gmail.com | ||
| 115 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 116 | + - id: 5 | ||
| 117 | + title: "agent.claude.yaml and EN+FR how-to + providers/configuration docs" | ||
| 118 | + state: open | ||
| 119 | + priority: low | ||
| 120 | + author: | ||
| 121 | + name: k33g | ||
| 122 | + email: ph.charriere@gmail.com | ||
| 123 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 14 | comments: [] | 124 | comments: [] |
added
.tickets/issues/0031-how-to-use-mini-me-with-gemini.yaml +109 -0 | new file mode 100644 | ||
| @@ -0,0 +1,109 @@ | ||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | |
| 2 | +id: 31 | |
| 3 | +title: How to use Mini Me with Gemini | |
| 4 | +state: open | |
| 5 | +author: | |
| 6 | + name: k33g | |
| 7 | + email: ph.charriere@gmail.com | |
| 8 | +createdAt: 2026-09-21T04:01:15.166Z | |
| 9 | +updatedAt: 2026-09-21T05:45:00.000Z | |
| 10 | +labels: [] | |
| 11 | +milestone: v0.0.3 | |
| 12 | +body: | | |
| 13 | + ## Analysis (Claude, 2026-09-21 — not implemented) | |
| 14 | + | |
| 15 | + **Recommendation: one registry entry on the existing `openaiCompat` implementation. Roughly 20 lines. Do this one first — it is the cheap, low-risk half of the pair with ticket 30.** | |
| 16 | + | |
| 17 | + ### Measured, not assumed | |
| 18 | + | |
| 19 | + Google's OpenAI-compatible layer is live at `https://generativelanguage.googleapis.com/v1beta/openai`: | |
| 20 | + | |
| 21 | + ``` | |
| 22 | + POST …/v1beta/openai/chat/completions → 400 "Missing or invalid Authorization header." | |
| 23 | + GET …/v1beta/openai/models → 404 without a key; 400 "Please pass a valid API key" with a bogus one | |
| 24 | + ``` | |
| 25 | + | |
| 26 | + Function calling is a first-class surface there, so unlike ticket 30 there is no open question about whether tool use survives. | |
| 27 | + | |
| 28 | + ### The entry | |
| 29 | + | |
| 30 | + In `internal/engine/openai_compat.go`'s `init()`, beside `dmr` and `llamacpp`: | |
| 31 | + | |
| 32 | + ``` | |
| 33 | + baseURL: "https://generativelanguage.googleapis.com/v1beta/openai" | |
| 34 | + apiKeyEnv: "GEMINI_API_KEY" | |
| 35 | + keyRequired: true | |
| 36 | + fallback: "" | |
| 37 | + ``` | |
| 38 | + | |
| 39 | + API-key handling then works untouched: `keyRequired` + `apiKeyEnv` + `Resolve` already produce the right refusal when the variable is unset. | |
| 40 | + | |
| 41 | + ### What does NOT carry over | |
| 42 | + | |
| 43 | + | Thing | What happens | Fix | | |
| 44 | + |---|---|---| | |
| 45 | + | **`Probe` / reachability** | `reachable()` sends no headers and demands a 200 on `/models`. Gemini never gives one. The banner would print `[warning: … nothing answers at …]` at **every** start-up. | Small, and the seam exists: `probe` is already a pluggable func field on `openaiCompat`. A `probeRemote` that treats 400/401/403 as "the host is there, we are just not authenticated". | | |
| 46 | + | **Context window** | No `/props` analogue, so `ctx: unknown` forever — and with it `main.go`'s "compression is on but will never trigger" warning. | `contextWindow:` must be set explicitly in the shipped YAML. | | |
| 47 | + | **Reasoning (ticket 29)** | `reasoningTap` scans for `delta.reasoning_content` / `delta.reasoning`. Gemini's compat layer is not known to emit either. Losing it loses the 💭 **and the watchdog heartbeat** — see ticket 30, same hazard, same mitigation. | Verify with a key; a generous `watchdogTimeout` in the YAML meanwhile. | | |
| 48 | + | **`Explain` hints** | Inherited free (it is a method on `*openaiCompat`), and the 401 branch already cites `apiKeyEnv`, which is right for a paid API. But `startHint`/`notFoundHint` need vendor words, the `jinja` branch is dead, and there is no branch for 400 (unknown model, unsupported parameter) or 402. | New words, no new structure. | | |
| 49 | + | **`toolCallGuard`** | Only accumulates when `ToolRequest.Input` is a string, i.e. partial-JSON argument deltas. If the endpoint delivers tool calls whole, the guard is inert — harmless, just no protection. | Nothing. | | |
| 50 | + | |
| 51 | + ### Config and docs | |
| 52 | + | |
| 53 | + **No change to `config.Config`** — `provider`, `baseUrl`, `apiKeyEnv`, `contextWindow`, `sampling`, `watchdogTimeout` already express everything. What is needed: the comment listing the known provider names; a shipped `agent.gemini.yaml` modelled on `agent.llamacpp.yaml` (concrete `model:`, `fallback: ""`, explicit `contextWindow:`, generous `watchdogTimeout:`); `docs/{en,fr}/explanation/providers.md` — its opening sentence still says "two servers"; `reference/configuration.md`; and a new `how-to/run-with-gemini.md` in both languages, the pattern every previous provider followed. | |
| 54 | + | |
| 55 | + ### Deliberately NOT recommended | |
| 56 | + | |
| 57 | + The native `plugins/googlegenai` route. It costs a new dependency tree (`google.golang.org/genai`, plus `cloud.google.com/go/auth`), a second code path in `Generate`, and a **silent** sampling mismatch: that plugin maps `sampling` onto `genai.GenerateContentConfig`, whose tags are camelCase (`topP`, `maxOutputTokens`), so mm's snake_case `top_p` and `max_tokens` would be dropped without a word. It would buy native reasoning parts — which the compat layer plus `reasoningTap` may already give. Not worth it for a capability the OpenAI layer provides. | |
| 58 | + | |
| 59 | + Note **Vertex AI does not fit `Backend` at all** — project + location + ADC, no `apiKeyEnv`, no `baseUrl`. If Vertex is wanted, that is a different ticket with new config keys. | |
| 60 | + | |
| 61 | + ### To decide before coding | |
| 62 | + | |
| 63 | + 1. Gemini API (`GEMINI_API_KEY`) or Vertex AI? One registry entry cannot serve both. | |
| 64 | + 2. Which model ids ship in the YAML, and are they pinned? Hosted ids go stale in months. | |
| 65 | + 3. **Is this really two tickets, or one?** A single generic `openai` entry where the user supplies `baseUrl` would cover Gemini, Claude, OpenRouter, Groq, Together and vLLM at once. It trades good per-vendor `Explain` words for reach. See ticket 30. | |
| 66 | + 4. `parallel_tool_calls: false` is in the shipped YAMLs — keep sending it to a remote provider that may reject or ignore it? | |
| 67 | + 5. Metered-API guardrails: `maxTurns: 40` plus a `bash` tool is a different proposition when every turn is billed. | |
| 68 | +tasks: | |
| 69 | + - id: 1 | |
| 70 | + title: "Registry entry for the Gemini OpenAI-compatible endpoint (baseURL, GEMINI_API_KEY, keyRequired)" | |
| 71 | + state: open | |
| 72 | + priority: medium | |
| 73 | + author: | |
| 74 | + name: k33g | |
| 75 | + email: ph.charriere@gmail.com | |
| 76 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 77 | + - id: 2 | |
| 78 | + title: "probeRemote: treat 400/401/403 as reachable, so the banner stops crying wolf" | |
| 79 | + state: open | |
| 80 | + priority: medium | |
| 81 | + author: | |
| 82 | + name: k33g | |
| 83 | + email: ph.charriere@gmail.com | |
| 84 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 85 | + - id: 3 | |
| 86 | + title: "Check whether the endpoint streams reasoning, and whether reasoningTap sees it" | |
| 87 | + state: open | |
| 88 | + priority: high | |
| 89 | + author: | |
| 90 | + name: k33g | |
| 91 | + email: ph.charriere@gmail.com | |
| 92 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 93 | + - id: 4 | |
| 94 | + title: "Explain: vendor words for startHint/notFoundHint, a 400 branch, drop the dead jinja branch" | |
| 95 | + state: open | |
| 96 | + priority: low | |
| 97 | + author: | |
| 98 | + name: k33g | |
| 99 | + email: ph.charriere@gmail.com | |
| 100 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 101 | + - id: 5 | |
| 102 | + title: "agent.gemini.yaml with an explicit contextWindow, and EN+FR how-to + providers/configuration docs" | |
| 103 | + state: open | |
| 104 | + priority: low | |
| 105 | + author: | |
| 106 | + name: k33g | |
| 107 | + email: ph.charriere@gmail.com | |
| 108 | + createdAt: 2026-09-21T05:45:00.000Z | |
| 109 | +comments: [] | |
| new file mode 100644 | |||
| @@ -0,0 +1,109 @@ | |||
| 1 | +# Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | ||
| 2 | +id: 31 | ||
| 3 | +title: How to use Mini Me with Gemini | ||
| 4 | +state: open | ||
| 5 | +author: | ||
| 6 | + name: k33g | ||
| 7 | + email: ph.charriere@gmail.com | ||
| 8 | +createdAt: 2026-09-21T04:01:15.166Z | ||
| 9 | +updatedAt: 2026-09-21T05:45:00.000Z | ||
| 10 | +labels: [] | ||
| 11 | +milestone: v0.0.3 | ||
| 12 | +body: | | ||
| 13 | + ## Analysis (Claude, 2026-09-21 — not implemented) | ||
| 14 | + | ||
| 15 | + **Recommendation: one registry entry on the existing `openaiCompat` implementation. Roughly 20 lines. Do this one first — it is the cheap, low-risk half of the pair with ticket 30.** | ||
| 16 | + | ||
| 17 | + ### Measured, not assumed | ||
| 18 | + | ||
| 19 | + Google's OpenAI-compatible layer is live at `https://generativelanguage.googleapis.com/v1beta/openai`: | ||
| 20 | + | ||
| 21 | + ``` | ||
| 22 | + POST …/v1beta/openai/chat/completions → 400 "Missing or invalid Authorization header." | ||
| 23 | + GET …/v1beta/openai/models → 404 without a key; 400 "Please pass a valid API key" with a bogus one | ||
| 24 | + ``` | ||
| 25 | + | ||
| 26 | + Function calling is a first-class surface there, so unlike ticket 30 there is no open question about whether tool use survives. | ||
| 27 | + | ||
| 28 | + ### The entry | ||
| 29 | + | ||
| 30 | + In `internal/engine/openai_compat.go`'s `init()`, beside `dmr` and `llamacpp`: | ||
| 31 | + | ||
| 32 | + ``` | ||
| 33 | + baseURL: "https://generativelanguage.googleapis.com/v1beta/openai" | ||
| 34 | + apiKeyEnv: "GEMINI_API_KEY" | ||
| 35 | + keyRequired: true | ||
| 36 | + fallback: "" | ||
| 37 | + ``` | ||
| 38 | + | ||
| 39 | + API-key handling then works untouched: `keyRequired` + `apiKeyEnv` + `Resolve` already produce the right refusal when the variable is unset. | ||
| 40 | + | ||
| 41 | + ### What does NOT carry over | ||
| 42 | + | ||
| 43 | + | Thing | What happens | Fix | | ||
| 44 | + |---|---|---| | ||
| 45 | + | **`Probe` / reachability** | `reachable()` sends no headers and demands a 200 on `/models`. Gemini never gives one. The banner would print `[warning: … nothing answers at …]` at **every** start-up. | Small, and the seam exists: `probe` is already a pluggable func field on `openaiCompat`. A `probeRemote` that treats 400/401/403 as "the host is there, we are just not authenticated". | | ||
| 46 | + | **Context window** | No `/props` analogue, so `ctx: unknown` forever — and with it `main.go`'s "compression is on but will never trigger" warning. | `contextWindow:` must be set explicitly in the shipped YAML. | | ||
| 47 | + | **Reasoning (ticket 29)** | `reasoningTap` scans for `delta.reasoning_content` / `delta.reasoning`. Gemini's compat layer is not known to emit either. Losing it loses the 💭 **and the watchdog heartbeat** — see ticket 30, same hazard, same mitigation. | Verify with a key; a generous `watchdogTimeout` in the YAML meanwhile. | | ||
| 48 | + | **`Explain` hints** | Inherited free (it is a method on `*openaiCompat`), and the 401 branch already cites `apiKeyEnv`, which is right for a paid API. But `startHint`/`notFoundHint` need vendor words, the `jinja` branch is dead, and there is no branch for 400 (unknown model, unsupported parameter) or 402. | New words, no new structure. | | ||
| 49 | + | **`toolCallGuard`** | Only accumulates when `ToolRequest.Input` is a string, i.e. partial-JSON argument deltas. If the endpoint delivers tool calls whole, the guard is inert — harmless, just no protection. | Nothing. | | ||
| 50 | + | ||
| 51 | + ### Config and docs | ||
| 52 | + | ||
| 53 | + **No change to `config.Config`** — `provider`, `baseUrl`, `apiKeyEnv`, `contextWindow`, `sampling`, `watchdogTimeout` already express everything. What is needed: the comment listing the known provider names; a shipped `agent.gemini.yaml` modelled on `agent.llamacpp.yaml` (concrete `model:`, `fallback: ""`, explicit `contextWindow:`, generous `watchdogTimeout:`); `docs/{en,fr}/explanation/providers.md` — its opening sentence still says "two servers"; `reference/configuration.md`; and a new `how-to/run-with-gemini.md` in both languages, the pattern every previous provider followed. | ||
| 54 | + | ||
| 55 | + ### Deliberately NOT recommended | ||
| 56 | + | ||
| 57 | + The native `plugins/googlegenai` route. It costs a new dependency tree (`google.golang.org/genai`, plus `cloud.google.com/go/auth`), a second code path in `Generate`, and a **silent** sampling mismatch: that plugin maps `sampling` onto `genai.GenerateContentConfig`, whose tags are camelCase (`topP`, `maxOutputTokens`), so mm's snake_case `top_p` and `max_tokens` would be dropped without a word. It would buy native reasoning parts — which the compat layer plus `reasoningTap` may already give. Not worth it for a capability the OpenAI layer provides. | ||
| 58 | + | ||
| 59 | + Note **Vertex AI does not fit `Backend` at all** — project + location + ADC, no `apiKeyEnv`, no `baseUrl`. If Vertex is wanted, that is a different ticket with new config keys. | ||
| 60 | + | ||
| 61 | + ### To decide before coding | ||
| 62 | + | ||
| 63 | + 1. Gemini API (`GEMINI_API_KEY`) or Vertex AI? One registry entry cannot serve both. | ||
| 64 | + 2. Which model ids ship in the YAML, and are they pinned? Hosted ids go stale in months. | ||
| 65 | + 3. **Is this really two tickets, or one?** A single generic `openai` entry where the user supplies `baseUrl` would cover Gemini, Claude, OpenRouter, Groq, Together and vLLM at once. It trades good per-vendor `Explain` words for reach. See ticket 30. | ||
| 66 | + 4. `parallel_tool_calls: false` is in the shipped YAMLs — keep sending it to a remote provider that may reject or ignore it? | ||
| 67 | + 5. Metered-API guardrails: `maxTurns: 40` plus a `bash` tool is a different proposition when every turn is billed. | ||
| 68 | +tasks: | ||
| 69 | + - id: 1 | ||
| 70 | + title: "Registry entry for the Gemini OpenAI-compatible endpoint (baseURL, GEMINI_API_KEY, keyRequired)" | ||
| 71 | + state: open | ||
| 72 | + priority: medium | ||
| 73 | + author: | ||
| 74 | + name: k33g | ||
| 75 | + email: ph.charriere@gmail.com | ||
| 76 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 77 | + - id: 2 | ||
| 78 | + title: "probeRemote: treat 400/401/403 as reachable, so the banner stops crying wolf" | ||
| 79 | + state: open | ||
| 80 | + priority: medium | ||
| 81 | + author: | ||
| 82 | + name: k33g | ||
| 83 | + email: ph.charriere@gmail.com | ||
| 84 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 85 | + - id: 3 | ||
| 86 | + title: "Check whether the endpoint streams reasoning, and whether reasoningTap sees it" | ||
| 87 | + state: open | ||
| 88 | + priority: high | ||
| 89 | + author: | ||
| 90 | + name: k33g | ||
| 91 | + email: ph.charriere@gmail.com | ||
| 92 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 93 | + - id: 4 | ||
| 94 | + title: "Explain: vendor words for startHint/notFoundHint, a 400 branch, drop the dead jinja branch" | ||
| 95 | + state: open | ||
| 96 | + priority: low | ||
| 97 | + author: | ||
| 98 | + name: k33g | ||
| 99 | + email: ph.charriere@gmail.com | ||
| 100 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 101 | + - id: 5 | ||
| 102 | + title: "agent.gemini.yaml with an explicit contextWindow, and EN+FR how-to + providers/configuration docs" | ||
| 103 | + state: open | ||
| 104 | + priority: low | ||
| 105 | + author: | ||
| 106 | + name: k33g | ||
| 107 | + email: ph.charriere@gmail.com | ||
| 108 | + createdAt: 2026-09-21T05:45:00.000Z | ||
| 109 | +comments: [] | ||
modified
.tickets/issues/0032-add-prompt-flag.yaml +2 -1 | @@ -6,8 +6,9 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T04:47:19.089Z |
| 9 | -updatedAt: 2026-09-21T04:47:19.089Z | |
| 9 | +updatedAt: 2026-09-21T04:58:31.828Z | |
| 10 | 10 | labels: [] |
| 11 | +milestone: v0.04 | |
| 11 | 12 | epic: CLI |
| 12 | 13 | body: "" |
| 13 | 14 | tasks: [] |
| @@ -6,8 +6,9 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T04:47:19.089Z | 8 | createdAt: 2026-09-21T04:47:19.089Z |
| 9 | -updatedAt: 2026-09-21T04:47:19.089Z | 9 | +updatedAt: 2026-09-21T04:58:31.828Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | +milestone: v0.04 | ||
| 11 | epic: CLI | 12 | epic: CLI |
| 12 | body: "" | 13 | body: "" |
| 13 | tasks: [] | 14 | tasks: [] |
modified
.tickets/issues/0033-invoke-another-mini-me.yaml +2 -1 | @@ -6,8 +6,9 @@ author: | ||
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T04:48:03.596Z |
| 9 | -updatedAt: 2026-09-21T04:48:35.612Z | |
| 9 | +updatedAt: 2026-09-21T04:58:54.762Z | |
| 10 | 10 | labels: [] |
| 11 | +milestone: v0.04 | |
| 11 | 12 | epic: Builtin tools |
| 12 | 13 | body: | |
| 13 | 14 | Should I use ACP ? I think yes |
| @@ -6,8 +6,9 @@ author: | |||
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T04:48:03.596Z | 8 | createdAt: 2026-09-21T04:48:03.596Z |
| 9 | -updatedAt: 2026-09-21T04:48:35.612Z | 9 | +updatedAt: 2026-09-21T04:58:54.762Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | +milestone: v0.04 | ||
| 11 | epic: Builtin tools | 12 | epic: Builtin tools |
| 12 | body: | | 13 | body: | |
| 13 | Should I use ACP ? I think yes | 14 | Should I use ACP ? I think yes |
renamed
.tickets/issues/0035-create-sbx-template-kit-for-mini-me.yaml +2 -2 | similarity index 74% | ||
| rename from .tickets/issues/0035-create-sbx-templat-kit-for-mini-me.yaml | ||
| rename to .tickets/issues/0035-create-sbx-template-kit-for-mini-me.yaml | ||
| @@ -1,12 +1,12 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | 2 | id: 35 |
| 3 | -title: Create SBX Templat + Kit for Mini-Me | |
| 3 | +title: Create SBX Template + Kit for Mini-Me | |
| 4 | 4 | state: open |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | 8 | createdAt: 2026-09-21T04:51:17.051Z |
| 9 | -updatedAt: 2026-09-21T04:51:17.051Z | |
| 9 | +updatedAt: 2026-09-21T04:55:19.972Z | |
| 10 | 10 | labels: [] |
| 11 | 11 | milestone: v0.0.3 |
| 12 | 12 | body: "" |
| similarity index 74% | |||
| rename from .tickets/issues/0035-create-sbx-templat-kit-for-mini-me.yaml | |||
| rename to .tickets/issues/0035-create-sbx-template-kit-for-mini-me.yaml | |||
| @@ -1,12 +1,12 @@ | |||
| 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | id: 35 | 2 | id: 35 |
| 3 | -title: Create SBX Templat + Kit for Mini-Me | 3 | +title: Create SBX Template + Kit for Mini-Me |
| 4 | state: open | 4 | state: open |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | createdAt: 2026-09-21T04:51:17.051Z | 8 | createdAt: 2026-09-21T04:51:17.051Z |
| 9 | -updatedAt: 2026-09-21T04:51:17.051Z | 9 | +updatedAt: 2026-09-21T04:55:19.972Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | milestone: v0.0.3 | 11 | milestone: v0.0.3 |
| 12 | body: "" | 12 | body: "" |
renamed
.tickets/issues/0036-hooks.yaml +5 -5 | similarity index 57% | ||
| rename from .tickets/issues/0031-hoe-to-use-mini-me-with-gemini.yaml | ||
| rename to .tickets/issues/0036-hooks.yaml | ||
| @@ -1,14 +1,14 @@ | ||
| 1 | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | -id: 31 | |
| 3 | -title: Hoe to use Mini Me with Gemini | |
| 2 | +id: 36 | |
| 3 | +title: Hooks | |
| 4 | 4 | state: open |
| 5 | 5 | author: |
| 6 | 6 | name: k33g |
| 7 | 7 | email: ph.charriere@gmail.com |
| 8 | -createdAt: 2026-09-21T04:01:15.166Z | |
| 9 | -updatedAt: 2026-09-21T04:01:15.166Z | |
| 8 | +createdAt: 2026-09-21T06:32:48.566Z | |
| 9 | +updatedAt: 2026-09-21T06:32:48.566Z | |
| 10 | 10 | labels: [] |
| 11 | -milestone: v0.0.3 | |
| 11 | +milestone: v0.04 | |
| 12 | 12 | body: "" |
| 13 | 13 | tasks: [] |
| 14 | 14 | comments: [] |
| similarity index 57% | |||
| rename from .tickets/issues/0031-hoe-to-use-mini-me-with-gemini.yaml | |||
| rename to .tickets/issues/0036-hooks.yaml | |||
| @@ -1,14 +1,14 @@ | |||
| 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. | 1 | # Managed by IssueSpec. Hand edits are welcome; keep the schema valid. |
| 2 | -id: 31 | 2 | +id: 36 |
| 3 | -title: Hoe to use Mini Me with Gemini | 3 | +title: Hooks |
| 4 | state: open | 4 | state: open |
| 5 | author: | 5 | author: |
| 6 | name: k33g | 6 | name: k33g |
| 7 | email: ph.charriere@gmail.com | 7 | email: ph.charriere@gmail.com |
| 8 | -createdAt: 2026-09-21T04:01:15.166Z | 8 | +createdAt: 2026-09-21T06:32:48.566Z |
| 9 | -updatedAt: 2026-09-21T04:01:15.166Z | 9 | +updatedAt: 2026-09-21T06:32:48.566Z |
| 10 | labels: [] | 10 | labels: [] |
| 11 | -milestone: v0.0.3 | 11 | +milestone: v0.04 |
| 12 | body: "" | 12 | body: "" |
| 13 | tasks: [] | 13 | tasks: [] |
| 14 | comments: [] | 14 | comments: [] |
modified
README.md +1 -0 | @@ -28,6 +28,7 @@ cd mini-me | ||
| 28 | 28 | ./install.sh # go build, then copies mm to /usr/local/bin |
| 29 | 29 | mm -h # the flags |
| 30 | 30 | mm --version # which mm this is |
| 31 | +mm --about # what it is, in one screen | |
| 31 | 32 | ``` |
| 32 | 33 | > `install.sh` works only on macOS and Linux |
| 33 | 34 | |
| @@ -28,6 +28,7 @@ cd mini-me | |||
| 28 | ./install.sh # go build, then copies mm to /usr/local/bin | 28 | ./install.sh # go build, then copies mm to /usr/local/bin |
| 29 | mm -h # the flags | 29 | mm -h # the flags |
| 30 | mm --version # which mm this is | 30 | mm --version # which mm this is |
| 31 | +mm --about # what it is, in one screen | ||
| 31 | ``` | 32 | ``` |
| 32 | > `install.sh` works only on macOS and Linux | 33 | > `install.sh` works only on macOS and Linux |
| 33 | 34 | ||
modified
docs/en/reference/cli.md +4 -2 | @@ -5,7 +5,7 @@ | ||
| 5 | 5 | ## Invocation |
| 6 | 6 | |
| 7 | 7 | ``` |
| 8 | -mm [-config <path>] [-acp] [-tui] [--version] [<path>] | |
| 8 | +mm [-config <path>] [-acp] [-tui] [--version] [--about] [<path>] | |
| 9 | 9 | ``` |
| 10 | 10 | |
| 11 | 11 | | Element | Type | Default | Description | |
| @@ -15,6 +15,7 @@ mm [-config <path>] [-acp] [-tui] [--version] [<path>] | ||
| 15 | 15 | | `-acp` | bool | `false` | Serve the Agent Client Protocol on stdin/stdout instead of the terminal REPL. | |
| 16 | 16 | | `-tui` | bool | `false` | Open the single-window mode (conversation above, input box below). Same effect as `tui: true` in the YAML; ignored with `-acp`. Falls back to the line mode when stdin or stdout is not a terminal. | |
| 17 | 17 | | `--version` | bool | `false` | Print the version and exit, before the configuration is read. `-version` is the same flag. | |
| 18 | +| `--about` | bool | `false` | Print what `mm` is — one screen: version, the three front ends, the tools, the model servers, where the project and the docs live — and exit, before the configuration is read. | | |
| 18 | 19 | | `-h` | — | — | Print the flag usage and exit. | |
| 19 | 20 | |
| 20 | 21 | ## Version |
| @@ -25,7 +26,7 @@ mm [-config <path>] [-acp] [-tui] [--version] [<path>] | ||
| 25 | 26 | mm v0.0.3 (go1.26.5, darwin/arm64) |
| 26 | 27 | ``` |
| 27 | 28 | |
| 28 | -The same version opens the start-up banner (`mm v0.0.3 | config: …`), which the window mode keeps as the conversation's first line, and sits in the window's status bar before the counters. | |
| 29 | +`mm --about` prints the same version inside a short description of the program. The version also opens the start-up banner (`mm v0.0.3 | config: …`), which the window mode keeps as the conversation's first line, and sits in the window's status bar before the counters. | |
| 29 | 30 | |
| 30 | 31 | | Where it comes from | What you see | |
| 31 | 32 | |---|---| |
| @@ -92,6 +93,7 @@ mm v0.0.3 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | ||
| 92 | 93 | | Screen | The terminal's alternate screen, released on exit; the terminal's own scrollback is not written to. | |
| 93 | 94 | | Banner | Not printed on stdout; shown as the first line of the conversation. | |
| 94 | 95 | | Spinner | Disabled; the rule under the conversation animates instead. | |
| 96 | +| Model name | The header shows a short form of the model identifier: registry host and organisation dropped, the `-gguf` format suffix dropped, the tag kept, capped at 28 characters (`huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M` → `qwen3.5-2b:Q4_K_M`). The full identifier is in the start-up banner, which the window keeps as the conversation's first line. | | |
| 95 | 97 | | Layout | Header line (agent, model `@` provider, context use as `ctx N% (used/window)` or `ctx ~used (window unknown)`, `session <id>` when sessions are on); conversation with a scroll bar; rule with state and key hints; 3-row input box; status bar (`F1 help · Ctrl-Y copy · F2 mouse: … · Ctrl-C quit` on the left, `<version> · N msgs · N tools` on the right — the version is dropped when the two sides would not fit, so the key hints are never truncated to make room for it). | |
| 96 | 98 | | Tool calls | `‣ <tool> <detail> <status> <time>` where status is `… pending`, `… waiting for permission`, `… running`, `✓ done` or `✗ failed`; output folded to `previewLines` lines (`… N more line(s)`); `+`/`-` lines of `write_file`/`edit_file` output coloured. | |
| 97 | 99 | | Thinking | A thinking model's reasoning is its own `‣ thinking` entry, dim italic in a different colour, before the answer. Hidden with `showThinking: false`. | |
| @@ -5,7 +5,7 @@ | |||
| 5 | ## Invocation | 5 | ## Invocation |
| 6 | 6 | ||
| 7 | ``` | 7 | ``` |
| 8 | -mm [-config <path>] [-acp] [-tui] [--version] [<path>] | 8 | +mm [-config <path>] [-acp] [-tui] [--version] [--about] [<path>] |
| 9 | ``` | 9 | ``` |
| 10 | 10 | ||
| 11 | | Element | Type | Default | Description | | 11 | | Element | Type | Default | Description | |
| @@ -15,6 +15,7 @@ mm [-config <path>] [-acp] [-tui] [--version] [<path>] | |||
| 15 | | `-acp` | bool | `false` | Serve the Agent Client Protocol on stdin/stdout instead of the terminal REPL. | | 15 | | `-acp` | bool | `false` | Serve the Agent Client Protocol on stdin/stdout instead of the terminal REPL. | |
| 16 | | `-tui` | bool | `false` | Open the single-window mode (conversation above, input box below). Same effect as `tui: true` in the YAML; ignored with `-acp`. Falls back to the line mode when stdin or stdout is not a terminal. | | 16 | | `-tui` | bool | `false` | Open the single-window mode (conversation above, input box below). Same effect as `tui: true` in the YAML; ignored with `-acp`. Falls back to the line mode when stdin or stdout is not a terminal. | |
| 17 | | `--version` | bool | `false` | Print the version and exit, before the configuration is read. `-version` is the same flag. | | 17 | | `--version` | bool | `false` | Print the version and exit, before the configuration is read. `-version` is the same flag. | |
| 18 | +| `--about` | bool | `false` | Print what `mm` is — one screen: version, the three front ends, the tools, the model servers, where the project and the docs live — and exit, before the configuration is read. | | ||
| 18 | | `-h` | — | — | Print the flag usage and exit. | | 19 | | `-h` | — | — | Print the flag usage and exit. | |
| 19 | 20 | ||
| 20 | ## Version | 21 | ## Version |
| @@ -25,7 +26,7 @@ mm [-config <path>] [-acp] [-tui] [--version] [<path>] | |||
| 25 | mm v0.0.3 (go1.26.5, darwin/arm64) | 26 | mm v0.0.3 (go1.26.5, darwin/arm64) |
| 26 | ``` | 27 | ``` |
| 27 | 28 | ||
| 28 | -The same version opens the start-up banner (`mm v0.0.3 | config: …`), which the window mode keeps as the conversation's first line, and sits in the window's status bar before the counters. | 29 | +`mm --about` prints the same version inside a short description of the program. The version also opens the start-up banner (`mm v0.0.3 | config: …`), which the window mode keeps as the conversation's first line, and sits in the window's status bar before the counters. |
| 29 | 30 | ||
| 30 | | Where it comes from | What you see | | 31 | | Where it comes from | What you see | |
| 31 | |---|---| | 32 | |---|---| |
| @@ -92,6 +93,7 @@ mm v0.0.3 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | |||
| 92 | | Screen | The terminal's alternate screen, released on exit; the terminal's own scrollback is not written to. | | 93 | | Screen | The terminal's alternate screen, released on exit; the terminal's own scrollback is not written to. | |
| 93 | | Banner | Not printed on stdout; shown as the first line of the conversation. | | 94 | | Banner | Not printed on stdout; shown as the first line of the conversation. | |
| 94 | | Spinner | Disabled; the rule under the conversation animates instead. | | 95 | | Spinner | Disabled; the rule under the conversation animates instead. | |
| 96 | +| Model name | The header shows a short form of the model identifier: registry host and organisation dropped, the `-gguf` format suffix dropped, the tag kept, capped at 28 characters (`huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M` → `qwen3.5-2b:Q4_K_M`). The full identifier is in the start-up banner, which the window keeps as the conversation's first line. | | ||
| 95 | | Layout | Header line (agent, model `@` provider, context use as `ctx N% (used/window)` or `ctx ~used (window unknown)`, `session <id>` when sessions are on); conversation with a scroll bar; rule with state and key hints; 3-row input box; status bar (`F1 help · Ctrl-Y copy · F2 mouse: … · Ctrl-C quit` on the left, `<version> · N msgs · N tools` on the right — the version is dropped when the two sides would not fit, so the key hints are never truncated to make room for it). | | 97 | | Layout | Header line (agent, model `@` provider, context use as `ctx N% (used/window)` or `ctx ~used (window unknown)`, `session <id>` when sessions are on); conversation with a scroll bar; rule with state and key hints; 3-row input box; status bar (`F1 help · Ctrl-Y copy · F2 mouse: … · Ctrl-C quit` on the left, `<version> · N msgs · N tools` on the right — the version is dropped when the two sides would not fit, so the key hints are never truncated to make room for it). | |
| 96 | | Tool calls | `‣ <tool> <detail> <status> <time>` where status is `… pending`, `… waiting for permission`, `… running`, `✓ done` or `✗ failed`; output folded to `previewLines` lines (`… N more line(s)`); `+`/`-` lines of `write_file`/`edit_file` output coloured. | | 98 | | Tool calls | `‣ <tool> <detail> <status> <time>` where status is `… pending`, `… waiting for permission`, `… running`, `✓ done` or `✗ failed`; output folded to `previewLines` lines (`… N more line(s)`); `+`/`-` lines of `write_file`/`edit_file` output coloured. | |
| 97 | | Thinking | A thinking model's reasoning is its own `‣ thinking` entry, dim italic in a different colour, before the answer. Hidden with `showThinking: false`. | | 99 | | Thinking | A thinking model's reasoning is its own `‣ thinking` entry, dim italic in a different colour, before the answer. Hidden with `showThinking: false`. | |
modified
docs/fr/reference/cli.md +4 -2 | @@ -5,7 +5,7 @@ | ||
| 5 | 5 | ## Invocation |
| 6 | 6 | |
| 7 | 7 | ``` |
| 8 | -mm [-config <chemin>] [-acp] [-tui] [--version] [<chemin>] | |
| 8 | +mm [-config <chemin>] [-acp] [-tui] [--version] [--about] [<chemin>] | |
| 9 | 9 | ``` |
| 10 | 10 | |
| 11 | 11 | | Élément | Type | Défaut | Description | |
| @@ -15,6 +15,7 @@ mm [-config <chemin>] [-acp] [-tui] [--version] [<chemin>] | ||
| 15 | 15 | | `-acp` | booléen | `false` | Sert l'Agent Client Protocol sur stdin/stdout au lieu du REPL du terminal. | |
| 16 | 16 | | `-tui` | booléen | `false` | Ouvre le mode fenêtre unique (conversation en haut, zone de saisie en bas). Même effet que `tui: true` dans le YAML ; ignoré avec `-acp`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal. | |
| 17 | 17 | | `--version` | bool | `false` | Affiche la version et quitte, avant la lecture de la configuration. `-version` est la même option. | |
| 18 | +| `--about` | bool | `false` | Affiche ce qu'est `mm` — un écran : version, les trois façades, les outils, les serveurs de modèles, où vivent le projet et la documentation — puis quitte, avant la lecture de la configuration. | | |
| 18 | 19 | | `-h` | — | — | Affiche l'usage des options et quitte. | |
| 19 | 20 | |
| 20 | 21 | ## Version |
| @@ -25,7 +26,7 @@ mm [-config <chemin>] [-acp] [-tui] [--version] [<chemin>] | ||
| 25 | 26 | mm v0.0.3 (go1.26.5, darwin/arm64) |
| 26 | 27 | ``` |
| 27 | 28 | |
| 28 | -La même version ouvre la bannière de démarrage (`mm v0.0.3 | config: …`), que le mode fenêtre conserve comme première ligne de la conversation, et figure dans la barre d'état de la fenêtre avant les compteurs. | |
| 29 | +`mm --about` affiche la même version à l'intérieur d'une courte description du programme. La version ouvre aussi la bannière de démarrage (`mm v0.0.3 | config: …`), que le mode fenêtre conserve comme première ligne de la conversation, et figure dans la barre d'état de la fenêtre avant les compteurs. | |
| 29 | 30 | |
| 30 | 31 | | D'où elle vient | Ce que vous voyez | |
| 31 | 32 | |---|---| |
| @@ -92,6 +93,7 @@ mm v0.0.3 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | ||
| 92 | 93 | | Écran | L'écran alternatif du terminal, rendu à la sortie ; l'historique de défilement du terminal n'est pas écrit. | |
| 93 | 94 | | Bannière | Non affichée sur stdout ; montrée comme première ligne de la conversation. | |
| 94 | 95 | | Spinner | Désactivé ; la règle sous la conversation s'anime à sa place. | |
| 96 | +| Nom du modèle | L'en-tête affiche une forme courte de l'identifiant : hôte du registre et organisation retirés, suffixe de format `-gguf` retiré, tag conservé, plafonné à 28 caractères (`huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M` → `qwen3.5-2b:Q4_K_M`). L'identifiant complet figure dans la bannière de démarrage, que la fenêtre conserve comme première ligne de la conversation. | | |
| 95 | 97 | | Disposition | Ligne d'en-tête (agent, modèle `@` fournisseur, usage du contexte en `ctx N% (utilisé/fenêtre)` ou `ctx ~utilisé (window unknown)`, `session <id>` quand les sessions sont actives) ; conversation avec barre de défilement ; règle avec l'état et les rappels de touches ; zone de saisie de 3 lignes ; barre d'état (`F1 help · Ctrl-Y copy · F2 mouse: … · Ctrl-C quit` à gauche, `<version> · N msgs · N tools` à droite — la version disparaît quand les deux côtés ne tiennent pas, pour que les rappels de touches ne soient jamais tronqués à cause d'elle). | |
| 96 | 98 | | Appels d'outils | `‣ <outil> <détail> <statut> <durée>` où le statut est `… pending`, `… waiting for permission`, `… running`, `✓ done` ou `✗ failed` ; sortie repliée à `previewLines` lignes (`… N more line(s)`) ; lignes `+`/`-` des sorties de `write_file`/`edit_file` colorées. | |
| 97 | 99 | | Réflexion | Le raisonnement d'un modèle à raisonnement forme son entrée `‣ thinking`, grisée, en italique et d'une autre couleur, avant la réponse. Masquée avec `showThinking: false`. | |
| @@ -5,7 +5,7 @@ | |||
| 5 | ## Invocation | 5 | ## Invocation |
| 6 | 6 | ||
| 7 | ``` | 7 | ``` |
| 8 | -mm [-config <chemin>] [-acp] [-tui] [--version] [<chemin>] | 8 | +mm [-config <chemin>] [-acp] [-tui] [--version] [--about] [<chemin>] |
| 9 | ``` | 9 | ``` |
| 10 | 10 | ||
| 11 | | Élément | Type | Défaut | Description | | 11 | | Élément | Type | Défaut | Description | |
| @@ -15,6 +15,7 @@ mm [-config <chemin>] [-acp] [-tui] [--version] [<chemin>] | |||
| 15 | | `-acp` | booléen | `false` | Sert l'Agent Client Protocol sur stdin/stdout au lieu du REPL du terminal. | | 15 | | `-acp` | booléen | `false` | Sert l'Agent Client Protocol sur stdin/stdout au lieu du REPL du terminal. | |
| 16 | | `-tui` | booléen | `false` | Ouvre le mode fenêtre unique (conversation en haut, zone de saisie en bas). Même effet que `tui: true` dans le YAML ; ignoré avec `-acp`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal. | | 16 | | `-tui` | booléen | `false` | Ouvre le mode fenêtre unique (conversation en haut, zone de saisie en bas). Même effet que `tui: true` dans le YAML ; ignoré avec `-acp`. Retombe sur le mode ligne quand stdin ou stdout n'est pas un terminal. | |
| 17 | | `--version` | bool | `false` | Affiche la version et quitte, avant la lecture de la configuration. `-version` est la même option. | | 17 | | `--version` | bool | `false` | Affiche la version et quitte, avant la lecture de la configuration. `-version` est la même option. | |
| 18 | +| `--about` | bool | `false` | Affiche ce qu'est `mm` — un écran : version, les trois façades, les outils, les serveurs de modèles, où vivent le projet et la documentation — puis quitte, avant la lecture de la configuration. | | ||
| 18 | | `-h` | — | — | Affiche l'usage des options et quitte. | | 19 | | `-h` | — | — | Affiche l'usage des options et quitte. | |
| 19 | 20 | ||
| 20 | ## Version | 21 | ## Version |
| @@ -25,7 +26,7 @@ mm [-config <chemin>] [-acp] [-tui] [--version] [<chemin>] | |||
| 25 | mm v0.0.3 (go1.26.5, darwin/arm64) | 26 | mm v0.0.3 (go1.26.5, darwin/arm64) |
| 26 | ``` | 27 | ``` |
| 27 | 28 | ||
| 28 | -La même version ouvre la bannière de démarrage (`mm v0.0.3 | config: …`), que le mode fenêtre conserve comme première ligne de la conversation, et figure dans la barre d'état de la fenêtre avant les compteurs. | 29 | +`mm --about` affiche la même version à l'intérieur d'une courte description du programme. La version ouvre aussi la bannière de démarrage (`mm v0.0.3 | config: …`), que le mode fenêtre conserve comme première ligne de la conversation, et figure dans la barre d'état de la fenêtre avant les compteurs. |
| 29 | 30 | ||
| 30 | | D'où elle vient | Ce que vous voyez | | 31 | | D'où elle vient | Ce que vous voyez | |
| 31 | |---|---| | 32 | |---|---| |
| @@ -92,6 +93,7 @@ mm v0.0.3 | config: agent.llamacpp.yaml | provider: llamacpp | model: jetbrains/ | |||
| 92 | | Écran | L'écran alternatif du terminal, rendu à la sortie ; l'historique de défilement du terminal n'est pas écrit. | | 93 | | Écran | L'écran alternatif du terminal, rendu à la sortie ; l'historique de défilement du terminal n'est pas écrit. | |
| 93 | | Bannière | Non affichée sur stdout ; montrée comme première ligne de la conversation. | | 94 | | Bannière | Non affichée sur stdout ; montrée comme première ligne de la conversation. | |
| 94 | | Spinner | Désactivé ; la règle sous la conversation s'anime à sa place. | | 95 | | Spinner | Désactivé ; la règle sous la conversation s'anime à sa place. | |
| 96 | +| Nom du modèle | L'en-tête affiche une forme courte de l'identifiant : hôte du registre et organisation retirés, suffixe de format `-gguf` retiré, tag conservé, plafonné à 28 caractères (`huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M` → `qwen3.5-2b:Q4_K_M`). L'identifiant complet figure dans la bannière de démarrage, que la fenêtre conserve comme première ligne de la conversation. | | ||
| 95 | | Disposition | Ligne d'en-tête (agent, modèle `@` fournisseur, usage du contexte en `ctx N% (utilisé/fenêtre)` ou `ctx ~utilisé (window unknown)`, `session <id>` quand les sessions sont actives) ; conversation avec barre de défilement ; règle avec l'état et les rappels de touches ; zone de saisie de 3 lignes ; barre d'état (`F1 help · Ctrl-Y copy · F2 mouse: … · Ctrl-C quit` à gauche, `<version> · N msgs · N tools` à droite — la version disparaît quand les deux côtés ne tiennent pas, pour que les rappels de touches ne soient jamais tronqués à cause d'elle). | | 97 | | Disposition | Ligne d'en-tête (agent, modèle `@` fournisseur, usage du contexte en `ctx N% (utilisé/fenêtre)` ou `ctx ~utilisé (window unknown)`, `session <id>` quand les sessions sont actives) ; conversation avec barre de défilement ; règle avec l'état et les rappels de touches ; zone de saisie de 3 lignes ; barre d'état (`F1 help · Ctrl-Y copy · F2 mouse: … · Ctrl-C quit` à gauche, `<version> · N msgs · N tools` à droite — la version disparaît quand les deux côtés ne tiennent pas, pour que les rappels de touches ne soient jamais tronqués à cause d'elle). | |
| 96 | | Appels d'outils | `‣ <outil> <détail> <statut> <durée>` où le statut est `… pending`, `… waiting for permission`, `… running`, `✓ done` ou `✗ failed` ; sortie repliée à `previewLines` lignes (`… N more line(s)`) ; lignes `+`/`-` des sorties de `write_file`/`edit_file` colorées. | | 98 | | Appels d'outils | `‣ <outil> <détail> <statut> <durée>` où le statut est `… pending`, `… waiting for permission`, `… running`, `✓ done` ou `✗ failed` ; sortie repliée à `previewLines` lignes (`… N more line(s)`) ; lignes `+`/`-` des sorties de `write_file`/`edit_file` colorées. | |
| 97 | | Réflexion | Le raisonnement d'un modèle à raisonnement forme son entrée `‣ thinking`, grisée, en italique et d'une autre couleur, avant la réponse. Masquée avec `showThinking: false`. | | 99 | | Réflexion | Le raisonnement d'un modèle à raisonnement forme son entrée `‣ thinking`, grisée, en italique et d'une autre couleur, avant la réponse. Masquée avec `showThinking: false`. | |
added
internal/tui/modelname.go +89 -0 | new file mode 100644 | ||
| @@ -0,0 +1,89 @@ | ||
| 1 | +package tui | |
| 2 | + | |
| 3 | +import "strings" | |
| 4 | + | |
| 5 | +// Model identifiers are addresses, not names. A local registry hands out | |
| 6 | +// things like | |
| 7 | +// | |
| 8 | +// huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | |
| 9 | +// | |
| 10 | +// seventy characters of which maybe thirty say anything, and the header is | |
| 11 | +// the one line that is on screen the whole session — next to the agent, the | |
| 12 | +// provider, the context gauge and the session id, all of which get pushed off | |
| 13 | +// the right edge by it (ticket 24). | |
| 14 | +// | |
| 15 | +// The full identifier stays reachable: the start-up banner prints it as it | |
| 16 | +// is, and the window keeps that banner as the conversation's first line. | |
| 17 | + | |
| 18 | +// modelWidth is how much of the header one model may take. Past it the name | |
| 19 | +// is cut with an ellipsis: a header that is all model is not a header. | |
| 20 | +const modelWidth = 28 | |
| 21 | + | |
| 22 | +// quantLike matches the tokens a GGUF quantisation suffix is made of — | |
| 23 | +// "q4_k_m", "bf16", "iq3_xxs", "f16". Used only to recognise a repeat of the | |
| 24 | +// tag inside the name, never to strip something that might be a real variant: | |
| 25 | +// "…-gguf-smashed" keeps its "smashed", which says the weights were pruned. | |
| 26 | +func quantLike(s string) bool { | |
| 27 | + s = strings.ToLower(s) | |
| 28 | + switch { | |
| 29 | + case strings.HasPrefix(s, "q") && len(s) > 1 && s[1] >= '0' && s[1] <= '9': | |
| 30 | + return true | |
| 31 | + case strings.HasPrefix(s, "iq") && len(s) > 2 && s[2] >= '0' && s[2] <= '9': | |
| 32 | + return true | |
| 33 | + case s == "bf16", s == "f16", s == "f32": | |
| 34 | + return true | |
| 35 | + } | |
| 36 | + return false | |
| 37 | +} | |
| 38 | + | |
| 39 | +// shortModel is the header's name for a model: the last path segment, without | |
| 40 | +// the file format, with the tag kept — the tag is the one part of the address | |
| 41 | +// that says what is actually loaded — and capped. | |
| 42 | +// | |
| 43 | +// huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M → qwen3.5-2b:Q4_K_M | |
| 44 | +// …/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M → mellum2-12b-a2.5b-ins…:Q4_K_M | |
| 45 | +// ai/smollm2 → smollm2 | |
| 46 | +// gpt-4o → gpt-4o | |
| 47 | +func shortModel(id string) string { | |
| 48 | + if id == "" { | |
| 49 | + return "" | |
| 50 | + } | |
| 51 | + // The registry host and the organisation are routing, not identity. | |
| 52 | + if i := strings.LastIndex(id, "/"); i >= 0 { | |
| 53 | + id = id[i+1:] | |
| 54 | + } | |
| 55 | + name, tag, hasTag := strings.Cut(id, ":") | |
| 56 | + name = dropFormat(name) | |
| 57 | + if !hasTag { | |
| 58 | + return clipName(name, modelWidth) | |
| 59 | + } | |
| 60 | + // The tag is kept whole and the name pays for it: ":Q4_K_M" is four fewer | |
| 61 | + // characters than the name would lose, and it is the part that changes | |
| 62 | + // between two otherwise identical models. | |
| 63 | + return clipName(name, max(modelWidth-len([]rune(tag))-1, 8)) + ":" + tag | |
| 64 | +} | |
| 65 | + | |
| 66 | +// dropFormat removes a trailing "-gguf", and a trailing "-gguf-<quant>" where | |
| 67 | +// the quantisation is already about to be shown as the tag. | |
| 68 | +func dropFormat(name string) string { | |
| 69 | + lower := strings.ToLower(name) | |
| 70 | + if i := strings.LastIndex(lower, "-gguf"); i > 0 { | |
| 71 | + rest := lower[i+len("-gguf"):] | |
| 72 | + if rest == "" || (strings.HasPrefix(rest, "-") && quantLike(rest[1:])) { | |
| 73 | + return name[:i] | |
| 74 | + } | |
| 75 | + } | |
| 76 | + return name | |
| 77 | +} | |
| 78 | + | |
| 79 | +// clipName cuts a name to width runes, ellipsis included in the count. | |
| 80 | +func clipName(name string, width int) string { | |
| 81 | + r := []rune(name) | |
| 82 | + if len(r) <= width { | |
| 83 | + return name | |
| 84 | + } | |
| 85 | + if width <= 1 { | |
| 86 | + return "…" | |
| 87 | + } | |
| 88 | + return string(r[:width-1]) + "…" | |
| 89 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,89 @@ | |||
| 1 | +package tui | ||
| 2 | + | ||
| 3 | +import "strings" | ||
| 4 | + | ||
| 5 | +// Model identifiers are addresses, not names. A local registry hands out | ||
| 6 | +// things like | ||
| 7 | +// | ||
| 8 | +// huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M | ||
| 9 | +// | ||
| 10 | +// seventy characters of which maybe thirty say anything, and the header is | ||
| 11 | +// the one line that is on screen the whole session — next to the agent, the | ||
| 12 | +// provider, the context gauge and the session id, all of which get pushed off | ||
| 13 | +// the right edge by it (ticket 24). | ||
| 14 | +// | ||
| 15 | +// The full identifier stays reachable: the start-up banner prints it as it | ||
| 16 | +// is, and the window keeps that banner as the conversation's first line. | ||
| 17 | + | ||
| 18 | +// modelWidth is how much of the header one model may take. Past it the name | ||
| 19 | +// is cut with an ellipsis: a header that is all model is not a header. | ||
| 20 | +const modelWidth = 28 | ||
| 21 | + | ||
| 22 | +// quantLike matches the tokens a GGUF quantisation suffix is made of — | ||
| 23 | +// "q4_k_m", "bf16", "iq3_xxs", "f16". Used only to recognise a repeat of the | ||
| 24 | +// tag inside the name, never to strip something that might be a real variant: | ||
| 25 | +// "…-gguf-smashed" keeps its "smashed", which says the weights were pruned. | ||
| 26 | +func quantLike(s string) bool { | ||
| 27 | + s = strings.ToLower(s) | ||
| 28 | + switch { | ||
| 29 | + case strings.HasPrefix(s, "q") && len(s) > 1 && s[1] >= '0' && s[1] <= '9': | ||
| 30 | + return true | ||
| 31 | + case strings.HasPrefix(s, "iq") && len(s) > 2 && s[2] >= '0' && s[2] <= '9': | ||
| 32 | + return true | ||
| 33 | + case s == "bf16", s == "f16", s == "f32": | ||
| 34 | + return true | ||
| 35 | + } | ||
| 36 | + return false | ||
| 37 | +} | ||
| 38 | + | ||
| 39 | +// shortModel is the header's name for a model: the last path segment, without | ||
| 40 | +// the file format, with the tag kept — the tag is the one part of the address | ||
| 41 | +// that says what is actually loaded — and capped. | ||
| 42 | +// | ||
| 43 | +// huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M → qwen3.5-2b:Q4_K_M | ||
| 44 | +// …/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M → mellum2-12b-a2.5b-ins…:Q4_K_M | ||
| 45 | +// ai/smollm2 → smollm2 | ||
| 46 | +// gpt-4o → gpt-4o | ||
| 47 | +func shortModel(id string) string { | ||
| 48 | + if id == "" { | ||
| 49 | + return "" | ||
| 50 | + } | ||
| 51 | + // The registry host and the organisation are routing, not identity. | ||
| 52 | + if i := strings.LastIndex(id, "/"); i >= 0 { | ||
| 53 | + id = id[i+1:] | ||
| 54 | + } | ||
| 55 | + name, tag, hasTag := strings.Cut(id, ":") | ||
| 56 | + name = dropFormat(name) | ||
| 57 | + if !hasTag { | ||
| 58 | + return clipName(name, modelWidth) | ||
| 59 | + } | ||
| 60 | + // The tag is kept whole and the name pays for it: ":Q4_K_M" is four fewer | ||
| 61 | + // characters than the name would lose, and it is the part that changes | ||
| 62 | + // between two otherwise identical models. | ||
| 63 | + return clipName(name, max(modelWidth-len([]rune(tag))-1, 8)) + ":" + tag | ||
| 64 | +} | ||
| 65 | + | ||
| 66 | +// dropFormat removes a trailing "-gguf", and a trailing "-gguf-<quant>" where | ||
| 67 | +// the quantisation is already about to be shown as the tag. | ||
| 68 | +func dropFormat(name string) string { | ||
| 69 | + lower := strings.ToLower(name) | ||
| 70 | + if i := strings.LastIndex(lower, "-gguf"); i > 0 { | ||
| 71 | + rest := lower[i+len("-gguf"):] | ||
| 72 | + if rest == "" || (strings.HasPrefix(rest, "-") && quantLike(rest[1:])) { | ||
| 73 | + return name[:i] | ||
| 74 | + } | ||
| 75 | + } | ||
| 76 | + return name | ||
| 77 | +} | ||
| 78 | + | ||
| 79 | +// clipName cuts a name to width runes, ellipsis included in the count. | ||
| 80 | +func clipName(name string, width int) string { | ||
| 81 | + r := []rune(name) | ||
| 82 | + if len(r) <= width { | ||
| 83 | + return name | ||
| 84 | + } | ||
| 85 | + if width <= 1 { | ||
| 86 | + return "…" | ||
| 87 | + } | ||
| 88 | + return string(r[:width-1]) + "…" | ||
| 89 | +} | ||
added
internal/tui/modelname_test.go +75 -0 | new file mode 100644 | ||
| @@ -0,0 +1,75 @@ | ||
| 1 | +package tui | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "strings" | |
| 5 | + "testing" | |
| 6 | +) | |
| 7 | + | |
| 8 | +func TestShortModel(t *testing.T) { | |
| 9 | + cases := []struct{ name, id, want string }{ | |
| 10 | + {"host and org go, format goes, tag stays", | |
| 11 | + "huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M", "qwen3.5-2b:Q4_K_M"}, | |
| 12 | + {"a name repeating its own quantisation drops it once", | |
| 13 | + "huggingface.co/unsloth/gemma-4-e2b-it-gguf:Q4_K_M", "gemma-4-e2b-it:Q4_K_M"}, | |
| 14 | + {"a real variant marker is NOT mistaken for a format suffix", | |
| 15 | + "huggingface.co/prunaai/meta-llama-guard-2-8b-gguf-smashed:Q4_K_M", | |
| 16 | + "meta-llama-guard-2-8…:Q4_K_M"}, | |
| 17 | + {"short ids are left alone", "ai/smollm2", "smollm2"}, | |
| 18 | + {"an id with no path at all", "gpt-4o", "gpt-4o"}, | |
| 19 | + {"no tag, no format", "huggingface.co/qwen/qwen3-embedding-0.6b", "qwen3-embedding-0.6b"}, | |
| 20 | + {"empty stays empty", "", ""}, | |
| 21 | + } | |
| 22 | + for _, tc := range cases { | |
| 23 | + t.Run(tc.name, func(t *testing.T) { | |
| 24 | + if got := shortModel(tc.id); got != tc.want { | |
| 25 | + t.Errorf("shortModel(%q)\n got %q\nwant %q", tc.id, got, tc.want) | |
| 26 | + } | |
| 27 | + }) | |
| 28 | + } | |
| 29 | +} | |
| 30 | + | |
| 31 | +// Whatever the id, the header's share of it is bounded — that is the ticket. | |
| 32 | +func TestShortModelIsBounded(t *testing.T) { | |
| 33 | + ids := []string{ | |
| 34 | + "huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M", | |
| 35 | + "huggingface.co/vendor/" + strings.Repeat("very-long-", 20) + "name:Q8_0", | |
| 36 | + strings.Repeat("x", 500), | |
| 37 | + } | |
| 38 | + for _, id := range ids { | |
| 39 | + got := shortModel(id) | |
| 40 | + if len([]rune(got)) > modelWidth+1 { // +1: the ":" before a kept tag | |
| 41 | + t.Errorf("shortModel(%.40q…) = %q, %d runes — the header is all model", id, got, len([]rune(got))) | |
| 42 | + } | |
| 43 | + } | |
| 44 | +} | |
| 45 | + | |
| 46 | +// The tag survives the cut: it is what tells two otherwise identical models | |
| 47 | +// apart, and it is the part a user checks. | |
| 48 | +func TestShortModelKeepsTheTag(t *testing.T) { | |
| 49 | + got := shortModel("huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M") | |
| 50 | + if !strings.HasSuffix(got, ":Q4_K_M") { | |
| 51 | + t.Errorf("shortModel = %q, want it to end with the tag", got) | |
| 52 | + } | |
| 53 | + if !strings.HasPrefix(got, "mellum2-12b") { | |
| 54 | + t.Errorf("shortModel = %q, want it to start with the model's own name", got) | |
| 55 | + } | |
| 56 | +} | |
| 57 | + | |
| 58 | +// The header uses it, and the rest of the header still fits beside it. | |
| 59 | +func TestHeaderUsesTheShortName(t *testing.T) { | |
| 60 | + r, _ := newTestRunner(t, &fakeModel{answer: "hi"}) | |
| 61 | + r.opts.ModelName = "huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M" | |
| 62 | + | |
| 63 | + got := r.header() | |
| 64 | + if strings.Contains(got, "huggingface.co") { | |
| 65 | + t.Errorf("the header still carries the registry host: %q", got) | |
| 66 | + } | |
| 67 | + if !strings.Contains(got, "qwen3.5-2b:Q4_K_M") { | |
| 68 | + t.Errorf("the header lost the model: %q", got) | |
| 69 | + } | |
| 70 | + for _, want := range []string{"mm ·", "@ p", "ctx"} { | |
| 71 | + if !strings.Contains(got, want) { | |
| 72 | + t.Errorf("the header lost %q: %q", want, got) | |
| 73 | + } | |
| 74 | + } | |
| 75 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,75 @@ | |||
| 1 | +package tui | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "strings" | ||
| 5 | + "testing" | ||
| 6 | +) | ||
| 7 | + | ||
| 8 | +func TestShortModel(t *testing.T) { | ||
| 9 | + cases := []struct{ name, id, want string }{ | ||
| 10 | + {"host and org go, format goes, tag stays", | ||
| 11 | + "huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M", "qwen3.5-2b:Q4_K_M"}, | ||
| 12 | + {"a name repeating its own quantisation drops it once", | ||
| 13 | + "huggingface.co/unsloth/gemma-4-e2b-it-gguf:Q4_K_M", "gemma-4-e2b-it:Q4_K_M"}, | ||
| 14 | + {"a real variant marker is NOT mistaken for a format suffix", | ||
| 15 | + "huggingface.co/prunaai/meta-llama-guard-2-8b-gguf-smashed:Q4_K_M", | ||
| 16 | + "meta-llama-guard-2-8…:Q4_K_M"}, | ||
| 17 | + {"short ids are left alone", "ai/smollm2", "smollm2"}, | ||
| 18 | + {"an id with no path at all", "gpt-4o", "gpt-4o"}, | ||
| 19 | + {"no tag, no format", "huggingface.co/qwen/qwen3-embedding-0.6b", "qwen3-embedding-0.6b"}, | ||
| 20 | + {"empty stays empty", "", ""}, | ||
| 21 | + } | ||
| 22 | + for _, tc := range cases { | ||
| 23 | + t.Run(tc.name, func(t *testing.T) { | ||
| 24 | + if got := shortModel(tc.id); got != tc.want { | ||
| 25 | + t.Errorf("shortModel(%q)\n got %q\nwant %q", tc.id, got, tc.want) | ||
| 26 | + } | ||
| 27 | + }) | ||
| 28 | + } | ||
| 29 | +} | ||
| 30 | + | ||
| 31 | +// Whatever the id, the header's share of it is bounded — that is the ticket. | ||
| 32 | +func TestShortModelIsBounded(t *testing.T) { | ||
| 33 | + ids := []string{ | ||
| 34 | + "huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M", | ||
| 35 | + "huggingface.co/vendor/" + strings.Repeat("very-long-", 20) + "name:Q8_0", | ||
| 36 | + strings.Repeat("x", 500), | ||
| 37 | + } | ||
| 38 | + for _, id := range ids { | ||
| 39 | + got := shortModel(id) | ||
| 40 | + if len([]rune(got)) > modelWidth+1 { // +1: the ":" before a kept tag | ||
| 41 | + t.Errorf("shortModel(%.40q…) = %q, %d runes — the header is all model", id, got, len([]rune(got))) | ||
| 42 | + } | ||
| 43 | + } | ||
| 44 | +} | ||
| 45 | + | ||
| 46 | +// The tag survives the cut: it is what tells two otherwise identical models | ||
| 47 | +// apart, and it is the part a user checks. | ||
| 48 | +func TestShortModelKeepsTheTag(t *testing.T) { | ||
| 49 | + got := shortModel("huggingface.co/jetbrains/mellum2-12b-a2.5b-instruct-gguf-q4_k_m:Q4_K_M") | ||
| 50 | + if !strings.HasSuffix(got, ":Q4_K_M") { | ||
| 51 | + t.Errorf("shortModel = %q, want it to end with the tag", got) | ||
| 52 | + } | ||
| 53 | + if !strings.HasPrefix(got, "mellum2-12b") { | ||
| 54 | + t.Errorf("shortModel = %q, want it to start with the model's own name", got) | ||
| 55 | + } | ||
| 56 | +} | ||
| 57 | + | ||
| 58 | +// The header uses it, and the rest of the header still fits beside it. | ||
| 59 | +func TestHeaderUsesTheShortName(t *testing.T) { | ||
| 60 | + r, _ := newTestRunner(t, &fakeModel{answer: "hi"}) | ||
| 61 | + r.opts.ModelName = "huggingface.co/unsloth/qwen3.5-2b-gguf:Q4_K_M" | ||
| 62 | + | ||
| 63 | + got := r.header() | ||
| 64 | + if strings.Contains(got, "huggingface.co") { | ||
| 65 | + t.Errorf("the header still carries the registry host: %q", got) | ||
| 66 | + } | ||
| 67 | + if !strings.Contains(got, "qwen3.5-2b:Q4_K_M") { | ||
| 68 | + t.Errorf("the header lost the model: %q", got) | ||
| 69 | + } | ||
| 70 | + for _, want := range []string{"mm ·", "@ p", "ctx"} { | ||
| 71 | + if !strings.Contains(got, want) { | ||
| 72 | + t.Errorf("the header lost %q: %q", want, got) | ||
| 73 | + } | ||
| 74 | + } | ||
| 75 | +} | ||
modified
internal/tui/turn.go +1 -1 | @@ -334,7 +334,7 @@ func (r *runner) record(f store.Failure) { | ||
| 334 | 334 | // session is being saved. |
| 335 | 335 | func (r *runner) header() string { |
| 336 | 336 | var b strings.Builder |
| 337 | - fmt.Fprintf(&b, "%s · %s @ %s", r.opts.AgentName, r.opts.ModelName, r.opts.Provider) | |
| 337 | + fmt.Fprintf(&b, "%s · %s @ %s", r.opts.AgentName, shortModel(r.opts.ModelName), r.opts.Provider) | |
| 338 | 338 | used := max(r.opts.Model.LastInputTokens(), compact.Estimate(r.messages)) |
| 339 | 339 | if w := r.opts.Window(); w > 0 { |
| 340 | 340 | fmt.Fprintf(&b, " · ctx %d%% (%s/%s)", used*100/w, compact.Kilo(used), compact.Kilo(w)) |
| @@ -334,7 +334,7 @@ func (r *runner) record(f store.Failure) { | |||
| 334 | // session is being saved. | 334 | // session is being saved. |
| 335 | func (r *runner) header() string { | 335 | func (r *runner) header() string { |
| 336 | var b strings.Builder | 336 | var b strings.Builder |
| 337 | - fmt.Fprintf(&b, "%s · %s @ %s", r.opts.AgentName, r.opts.ModelName, r.opts.Provider) | 337 | + fmt.Fprintf(&b, "%s · %s @ %s", r.opts.AgentName, shortModel(r.opts.ModelName), r.opts.Provider) |
| 338 | used := max(r.opts.Model.LastInputTokens(), compact.Estimate(r.messages)) | 338 | used := max(r.opts.Model.LastInputTokens(), compact.Estimate(r.messages)) |
| 339 | if w := r.opts.Window(); w > 0 { | 339 | if w := r.opts.Window(); w > 0 { |
| 340 | fmt.Fprintf(&b, " · ctx %d%% (%s/%s)", used*100/w, compact.Kilo(used), compact.Kilo(w)) | 340 | fmt.Fprintf(&b, " · ctx %d%% (%s/%s)", used*100/w, compact.Kilo(used), compact.Kilo(w)) |
added
internal/version/about.go +34 -0 | new file mode 100644 | ||
| @@ -0,0 +1,34 @@ | ||
| 1 | +package version | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "fmt" | |
| 5 | + "strings" | |
| 6 | +) | |
| 7 | + | |
| 8 | +// About is what `mm --about` prints: what this program is, which one it is, | |
| 9 | +// and where to go next. It lives beside Short and Long because it is the same | |
| 10 | +// question asked with more room — "what am I running?" — and because a binary | |
| 11 | +// that can say what it is needs no network and no file to do it. | |
| 12 | +// | |
| 13 | +// Deliberately short. An --about that scrolls is a README in the wrong place, | |
| 14 | +// and the README is one link away. | |
| 15 | + | |
| 16 | +// Home is where the project lives. A constant, not a guess from the module | |
| 17 | +// path: the module is named `mm`, which says nothing about where it came from. | |
| 18 | +const Home = "https://git.rickub.com/bots-garden/mini-me" | |
| 19 | + | |
| 20 | +// About returns the block, ending without a trailing newline. | |
| 21 | +func About() string { | |
| 22 | + var b strings.Builder | |
| 23 | + fmt.Fprintf(&b, "Mini Me (mm) %s\n", Short()) | |
| 24 | + b.WriteString("A minimalist coding agent for local LLMs, written in Go.\n\n") | |
| 25 | + b.WriteString("One agent loop, three front ends:\n") | |
| 26 | + b.WriteString(" mm [config.yaml] a terminal REPL, line by line\n") | |
| 27 | + b.WriteString(" mm -tui [config.yaml] one terminal window: conversation, input box, status bar\n") | |
| 28 | + b.WriteString(" mm -acp the Agent Client Protocol on stdio, for Zed and other editors\n\n") | |
| 29 | + b.WriteString("Tools the model can call: bash, read_skill, read_file, write_file, edit_file, lsp.\n") | |
| 30 | + b.WriteString("Model servers: Docker Model Runner, llama.cpp — anything speaking the OpenAI API.\n\n") | |
| 31 | + fmt.Fprintf(&b, "%s\n", Home) | |
| 32 | + b.WriteString("Docs: docs/en (English), docs/fr (français) · mm -h for the flags") | |
| 33 | + return b.String() | |
| 34 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,34 @@ | |||
| 1 | +package version | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "fmt" | ||
| 5 | + "strings" | ||
| 6 | +) | ||
| 7 | + | ||
| 8 | +// About is what `mm --about` prints: what this program is, which one it is, | ||
| 9 | +// and where to go next. It lives beside Short and Long because it is the same | ||
| 10 | +// question asked with more room — "what am I running?" — and because a binary | ||
| 11 | +// that can say what it is needs no network and no file to do it. | ||
| 12 | +// | ||
| 13 | +// Deliberately short. An --about that scrolls is a README in the wrong place, | ||
| 14 | +// and the README is one link away. | ||
| 15 | + | ||
| 16 | +// Home is where the project lives. A constant, not a guess from the module | ||
| 17 | +// path: the module is named `mm`, which says nothing about where it came from. | ||
| 18 | +const Home = "https://git.rickub.com/bots-garden/mini-me" | ||
| 19 | + | ||
| 20 | +// About returns the block, ending without a trailing newline. | ||
| 21 | +func About() string { | ||
| 22 | + var b strings.Builder | ||
| 23 | + fmt.Fprintf(&b, "Mini Me (mm) %s\n", Short()) | ||
| 24 | + b.WriteString("A minimalist coding agent for local LLMs, written in Go.\n\n") | ||
| 25 | + b.WriteString("One agent loop, three front ends:\n") | ||
| 26 | + b.WriteString(" mm [config.yaml] a terminal REPL, line by line\n") | ||
| 27 | + b.WriteString(" mm -tui [config.yaml] one terminal window: conversation, input box, status bar\n") | ||
| 28 | + b.WriteString(" mm -acp the Agent Client Protocol on stdio, for Zed and other editors\n\n") | ||
| 29 | + b.WriteString("Tools the model can call: bash, read_skill, read_file, write_file, edit_file, lsp.\n") | ||
| 30 | + b.WriteString("Model servers: Docker Model Runner, llama.cpp — anything speaking the OpenAI API.\n\n") | ||
| 31 | + fmt.Fprintf(&b, "%s\n", Home) | ||
| 32 | + b.WriteString("Docs: docs/en (English), docs/fr (français) · mm -h for the flags") | ||
| 33 | + return b.String() | ||
| 34 | +} | ||
added
internal/version/about_test.go +40 -0 | new file mode 100644 | ||
| @@ -0,0 +1,40 @@ | ||
| 1 | +package version | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "strings" | |
| 5 | + "testing" | |
| 6 | +) | |
| 7 | + | |
| 8 | +// --about answers "what am I running?", so the two things that change between | |
| 9 | +// two binaries — the name and the version — must both be in it, and it must | |
| 10 | +// stay short enough to read without scrolling. | |
| 11 | +func TestAbout(t *testing.T) { | |
| 12 | + saved := stamped | |
| 13 | + t.Cleanup(func() { stamped = saved }) | |
| 14 | + stamped = "v0.0.3" | |
| 15 | + | |
| 16 | + got := About() | |
| 17 | + for _, want := range []string{"Mini Me", "mm", "v0.0.3", Home, "-tui", "-acp", "bash"} { | |
| 18 | + if !strings.Contains(got, want) { | |
| 19 | + t.Errorf("About() does not mention %q:\n%s", want, got) | |
| 20 | + } | |
| 21 | + } | |
| 22 | + if n := strings.Count(got, "\n") + 1; n > 20 { | |
| 23 | + t.Errorf("About() is %d lines: that is a README, not an --about", n) | |
| 24 | + } | |
| 25 | + if strings.HasSuffix(got, "\n") { | |
| 26 | + t.Error("About() ends with a newline; the caller's Println adds one") | |
| 27 | + } | |
| 28 | +} | |
| 29 | + | |
| 30 | +// It works on a binary with no stamp — the case for anyone building from | |
| 31 | +// source, which is most people reading it. | |
| 32 | +func TestAboutWithoutAStamp(t *testing.T) { | |
| 33 | + saved := stamped | |
| 34 | + t.Cleanup(func() { stamped = saved }) | |
| 35 | + stamped = "" | |
| 36 | + | |
| 37 | + if got := About(); !strings.Contains(got, Short()) { | |
| 38 | + t.Errorf("About() does not carry the version it reports:\n%s", got) | |
| 39 | + } | |
| 40 | +} | |
| new file mode 100644 | |||
| @@ -0,0 +1,40 @@ | |||
| 1 | +package version | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "strings" | ||
| 5 | + "testing" | ||
| 6 | +) | ||
| 7 | + | ||
| 8 | +// --about answers "what am I running?", so the two things that change between | ||
| 9 | +// two binaries — the name and the version — must both be in it, and it must | ||
| 10 | +// stay short enough to read without scrolling. | ||
| 11 | +func TestAbout(t *testing.T) { | ||
| 12 | + saved := stamped | ||
| 13 | + t.Cleanup(func() { stamped = saved }) | ||
| 14 | + stamped = "v0.0.3" | ||
| 15 | + | ||
| 16 | + got := About() | ||
| 17 | + for _, want := range []string{"Mini Me", "mm", "v0.0.3", Home, "-tui", "-acp", "bash"} { | ||
| 18 | + if !strings.Contains(got, want) { | ||
| 19 | + t.Errorf("About() does not mention %q:\n%s", want, got) | ||
| 20 | + } | ||
| 21 | + } | ||
| 22 | + if n := strings.Count(got, "\n") + 1; n > 20 { | ||
| 23 | + t.Errorf("About() is %d lines: that is a README, not an --about", n) | ||
| 24 | + } | ||
| 25 | + if strings.HasSuffix(got, "\n") { | ||
| 26 | + t.Error("About() ends with a newline; the caller's Println adds one") | ||
| 27 | + } | ||
| 28 | +} | ||
| 29 | + | ||
| 30 | +// It works on a binary with no stamp — the case for anyone building from | ||
| 31 | +// source, which is most people reading it. | ||
| 32 | +func TestAboutWithoutAStamp(t *testing.T) { | ||
| 33 | + saved := stamped | ||
| 34 | + t.Cleanup(func() { stamped = saved }) | ||
| 35 | + stamped = "" | ||
| 36 | + | ||
| 37 | + if got := About(); !strings.Contains(got, Short()) { | ||
| 38 | + t.Errorf("About() does not carry the version it reports:\n%s", got) | ||
| 39 | + } | ||
| 40 | +} | ||
modified
main.go +7 -0 | @@ -67,6 +67,7 @@ func main() { | ||
| 67 | 67 | acpMode := flag.Bool("acp", false, "serve the Agent Client Protocol on stdio (for Zed, JetBrains, Neovim…) instead of the terminal REPL") |
| 68 | 68 | tuiMode := flag.Bool("tui", false, "open the single-window mode (conversation above, input box below); falls back to the line mode when stdout is not a terminal") |
| 69 | 69 | showVersion := flag.Bool("version", false, "print the version and exit") |
| 70 | + showAbout := flag.Bool("about", false, "print what mm is, and exit") | |
| 70 | 71 | flag.Parse() |
| 71 | 72 | // Answered before anything can go wrong: asking which mm this is must |
| 72 | 73 | // work without a config file, without a model server, and in a directory |
| @@ -76,6 +77,12 @@ func main() { | ||
| 76 | 77 | fmt.Println(version.Long()) |
| 77 | 78 | return |
| 78 | 79 | } |
| 80 | + // Same rule as --version: answered before the configuration is looked | |
| 81 | + // for, so it works in any directory with no model server running. | |
| 82 | + if *showAbout { | |
| 83 | + fmt.Println(version.About()) | |
| 84 | + return | |
| 85 | + } | |
| 79 | 86 | if *configPath == "" && flag.NArg() > 0 { |
| 80 | 87 | *configPath = flag.Arg(0) |
| 81 | 88 | } |
| @@ -67,6 +67,7 @@ func main() { | |||
| 67 | acpMode := flag.Bool("acp", false, "serve the Agent Client Protocol on stdio (for Zed, JetBrains, Neovim…) instead of the terminal REPL") | 67 | acpMode := flag.Bool("acp", false, "serve the Agent Client Protocol on stdio (for Zed, JetBrains, Neovim…) instead of the terminal REPL") |
| 68 | tuiMode := flag.Bool("tui", false, "open the single-window mode (conversation above, input box below); falls back to the line mode when stdout is not a terminal") | 68 | tuiMode := flag.Bool("tui", false, "open the single-window mode (conversation above, input box below); falls back to the line mode when stdout is not a terminal") |
| 69 | showVersion := flag.Bool("version", false, "print the version and exit") | 69 | showVersion := flag.Bool("version", false, "print the version and exit") |
| 70 | + showAbout := flag.Bool("about", false, "print what mm is, and exit") | ||
| 70 | flag.Parse() | 71 | flag.Parse() |
| 71 | // Answered before anything can go wrong: asking which mm this is must | 72 | // Answered before anything can go wrong: asking which mm this is must |
| 72 | // work without a config file, without a model server, and in a directory | 73 | // work without a config file, without a model server, and in a directory |
| @@ -76,6 +77,12 @@ func main() { | |||
| 76 | fmt.Println(version.Long()) | 77 | fmt.Println(version.Long()) |
| 77 | return | 78 | return |
| 78 | } | 79 | } |
| 80 | + // Same rule as --version: answered before the configuration is looked | ||
| 81 | + // for, so it works in any directory with no model server running. | ||
| 82 | + if *showAbout { | ||
| 83 | + fmt.Println(version.About()) | ||
| 84 | + return | ||
| 85 | + } | ||
| 79 | if *configPath == "" && flag.NArg() > 0 { | 86 | if *configPath == "" && flag.NArg() > 0 { |
| 80 | *configPath = flag.Arg(0) | 87 | *configPath = flag.Arg(0) |
| 81 | } | 88 | } |