bots-garden/mini-mepublic Fork
⑂ 4d8eac0
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/mini-me.git
git clone ssh://git@rickub.com/bots-garden/mini-me.git

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

Download ZIP

Clear qlty lint findings: errcheck, dead MCP/plan code, staticcheck, shellcheck

Unchecked fmt.Fprint/Close results made explicit with blank assignments
(the project's existing convention), test helpers check CreatePlan/NextTask
errors, unused stubs in internal/tools removed, deprecated genkit.DefineModel
replaced in tests, tagged switches, narrow nolint for the LSP MarkedString
union servers still send.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
k33g committed 2026-10-16T09:00:30+02:00 Browse files
4d8eac0 parent: 5b2f986
modified 02-build-releases.sh +1 -0
@@ -134,6 +134,7 @@ echo " ✅ SHA256SUMS"
134134 downloadTable() {
135135 printf '| Platform | Download |\n|---|---|\n'
136136 for platform in "${PLATFORMS[@]}"; do
137+ # shellcheck disable=SC2016 # the backticks are Markdown code spans, not a command substitution
137138 local goos="${platform%/*}" goarch="${platform#*/}"
138139 printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")"
139140 done
@@ -134,6 +134,7 @@ echo " ✅ SHA256SUMS"
134 downloadTable() {134 downloadTable() {
135 printf '| Platform | Download |\n|---|---|\n'135 printf '| Platform | Download |\n|---|---|\n'
136 for platform in "${PLATFORMS[@]}"; do136 for platform in "${PLATFORMS[@]}"; do
137+ # shellcheck disable=SC2016 # the backticks are Markdown code spans, not a command substitution
137 local goos="${platform%/*}" goarch="${platform#*/}"138 local goos="${platform%/*}" goarch="${platform#*/}"
138 printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")"139 printf '| %s | `%s` |\n' "${platform}" "$(assetName "${goos}" "${goarch}")"
139 done140 done
modified demos/dungeon/guard.sh +1 -0
@@ -39,6 +39,7 @@ case "$kind" in
3939 command=$rest
4040 rewrite=1
4141 fi
42+ # shellcheck disable=SC2016 # '$(' is the literal text being refused, not an expansion
4243 case "$command" in
4344 *';'*|*'&'*|*'|'*|*'`'*|*'$('*|*'>'*|*'<'*|*'\n'*)
4445 deny "one command at a time, nothing chained or redirected. Your commands: $hint" ;;
@@ -39,6 +39,7 @@ case "$kind" in
39 command=$rest39 command=$rest
40 rewrite=140 rewrite=1
41 fi41 fi
42+ # shellcheck disable=SC2016 # '$(' is the literal text being refused, not an expansion
42 case "$command" in43 case "$command" in
43 *';'*|*'&'*|*'|'*|*'`'*|*'$('*|*'>'*|*'<'*|*'\n'*)44 *';'*|*'&'*|*'|'*|*'`'*|*'$('*|*'>'*|*'<'*|*'\n'*)
44 deny "one command at a time, nothing chained or redirected. Your commands: $hint" ;;45 deny "one command at a time, nothing chained or redirected. Your commands: $hint" ;;
modified demos/dungeon/sheet.sh +3 -1
@@ -135,7 +135,9 @@ meet() {
135135 }
136136
137137 monster() {
138- [ $# -eq 2 ] && is_signed "$2" || die 'usage: sh sheet.sh monster "<Name>" -5'
138+ if [ $# -ne 2 ] || ! is_signed "$2"; then
139+ die 'usage: sh sheet.sh monster "<Name>" -5'
140+ fi
139141 not_after_attack
140142 line=$(sed -n '/^## Encounter/,$p' "$SHEET" | grep -F -- "- $1:" | head -1 || true)
141143 [ -n "$line" ] || die "no monster \"$1\" in ## Encounter"
@@ -135,7 +135,9 @@ meet() {
135 }135 }
136 136
137 monster() {137 monster() {
138- [ $# -eq 2 ] && is_signed "$2" || die 'usage: sh sheet.sh monster "<Name>" -5'138+ if [ $# -ne 2 ] || ! is_signed "$2"; then
139+ die 'usage: sh sheet.sh monster "<Name>" -5'
140+ fi
139 not_after_attack141 not_after_attack
140 line=$(sed -n '/^## Encounter/,$p' "$SHEET" | grep -F -- "- $1:" | head -1 || true)142 line=$(sed -n '/^## Encounter/,$p' "$SHEET" | grep -F -- "- $1:" | head -1 || true)
141 [ -n "$line" ] || die "no monster \"$1\" in ## Encounter"143 [ -n "$line" ] || die "no monster \"$1\" in ## Encounter"
modified internal/agent/agent.go +0 -3
@@ -571,9 +571,6 @@ func RunPrompt(ctx context.Context, e *engine.Engine, system string, tools []ai.
571571 return
572572 }
573573
574- // Process and display the result
575- messages = session.AfterTurn(messages, before, resp, history, err)
576-
577574 // Display command stats like the regular mode
578575 turn := history[min(before, len(history)):]
579576 cmds := engine.CommandList(turn)
@@ -571,9 +571,6 @@ func RunPrompt(ctx context.Context, e *engine.Engine, system string, tools []ai.
571 return571 return
572 }572 }
573 573
574- // Process and display the result
575- messages = session.AfterTurn(messages, before, resp, history, err)
576-
577 // Display command stats like the regular mode574 // Display command stats like the regular mode
578 turn := history[min(before, len(history)):]575 turn := history[min(before, len(history)):]
579 cmds := engine.CommandList(turn)576 cmds := engine.CommandList(turn)
modified internal/agent/structured_test.go +3 -3
@@ -19,9 +19,9 @@ func fakeModel(t *testing.T, content string) *engine.Engine {
1919 t.Helper()
2020 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
2121 w.Header().Set("Content-Type", "text/event-stream")
22- fmt.Fprintf(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":%s}}]}\n\n", strconv.Quote(content))
23- fmt.Fprint(w, "data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n")
24- fmt.Fprint(w, "data: [DONE]\n\n")
22+ _, _ = fmt.Fprintf(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":%s}}]}\n\n", strconv.Quote(content))
23+ _, _ = fmt.Fprint(w, "data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n")
24+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
2525 }))
2626 t.Cleanup(srv.Close)
2727 p, err := engine.Lookup("llamacpp")
@@ -19,9 +19,9 @@ func fakeModel(t *testing.T, content string) *engine.Engine {
19 t.Helper()19 t.Helper()
20 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {20 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
21 w.Header().Set("Content-Type", "text/event-stream")21 w.Header().Set("Content-Type", "text/event-stream")
22- fmt.Fprintf(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":%s}}]}\n\n", strconv.Quote(content))22+ _, _ = fmt.Fprintf(w, "data: {\"choices\":[{\"index\":0,\"delta\":{\"role\":\"assistant\",\"content\":%s}}]}\n\n", strconv.Quote(content))
23- fmt.Fprint(w, "data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n")23+ _, _ = fmt.Fprint(w, "data: {\"choices\":[{\"index\":0,\"delta\":{},\"finish_reason\":\"stop\"}]}\n\n")
24- fmt.Fprint(w, "data: [DONE]\n\n")24+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
25 }))25 }))
26 t.Cleanup(srv.Close)26 t.Cleanup(srv.Close)
27 p, err := engine.Lookup("llamacpp")27 p, err := engine.Lookup("llamacpp")
modified internal/agentsmd/example_test.go +1 -1
@@ -12,7 +12,7 @@ import (
1212
1313 func ExampleSystem() {
1414 dir, _ := os.MkdirTemp("", "project")
15- defer os.RemoveAll(dir)
15+ defer func() { _ = os.RemoveAll(dir) }()
1616 _ = os.WriteFile(filepath.Join(dir, "AGENTS.md"), []byte("Run `make test` before committing."), 0o644)
1717 config.Cfg.AgentsMd = config.AgentsMdConfig{Enabled: true, Path: "AGENTS.md"}
1818
@@ -12,7 +12,7 @@ import (
12 12
13 func ExampleSystem() {13 func ExampleSystem() {
14 dir, _ := os.MkdirTemp("", "project")14 dir, _ := os.MkdirTemp("", "project")
15- defer os.RemoveAll(dir)15+ defer func() { _ = os.RemoveAll(dir) }()
16 _ = os.WriteFile(filepath.Join(dir, "AGENTS.md"), []byte("Run `make test` before committing."), 0o644)16 _ = os.WriteFile(filepath.Join(dir, "AGENTS.md"), []byte("Run `make test` before committing."), 0o644)
17 config.Cfg.AgentsMd = config.AgentsMdConfig{Enabled: true, Path: "AGENTS.md"}17 config.Cfg.AgentsMd = config.AgentsMdConfig{Enabled: true, Path: "AGENTS.md"}
18 18
modified internal/ask/cli.go +7 -7
@@ -89,8 +89,8 @@ func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stde
8989 if req.WithContext {
9090 label += ", with the conversation"
9191 }
92- fmt.Fprintln(stderr, label+"…")
93- p.notify = func(line string) { fmt.Fprintln(stderr, line) }
92+ _, _ = fmt.Fprintln(stderr, label+"…")
93+ p.notify = func(line string) { _, _ = fmt.Fprintln(stderr, line) }
9494 // With postAsk hooks the answer is held back until they have run: what
9595 // stdout carries is what the caller reads, and a hook may replace it.
9696 var held strings.Builder
@@ -103,10 +103,10 @@ func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stde
103103 answer := res.Answer
104104 if !streamed {
105105 answer = res.Text()
106- fmt.Fprint(stdout, answer)
106+ _, _ = fmt.Fprint(stdout, answer)
107107 }
108108 if answer != "" && !strings.HasSuffix(answer, "\n") {
109- fmt.Fprintln(stdout)
109+ _, _ = fmt.Fprintln(stdout)
110110 }
111111 if err == nil && res.Answer == "" {
112112 return errors.New(req.Agent + " gave no answer")
@@ -135,14 +135,14 @@ type cliSink struct {
135135
136136 var _ ui.Sink = (*cliSink)(nil)
137137
138-func (s *cliSink) Text(text string) { fmt.Fprint(s.out, text) }
138+func (s *cliSink) Text(text string) { _, _ = fmt.Fprint(s.out, text) }
139139 func (s *cliSink) Reasoning(string) {}
140140 func (s *cliSink) ToolStart(ev ui.ToolEvent) {
141- fmt.Fprintln(s.log, "🛠️ "+ev.Title)
141+ _, _ = fmt.Fprintln(s.log, "🛠️ "+ev.Title)
142142 }
143143 func (s *cliSink) ToolEnd(_ string, res ui.ToolResult) {
144144 if res.Failed {
145- fmt.Fprintln(s.log, " │ failed")
145+ _, _ = fmt.Fprintln(s.log, " │ failed")
146146 }
147147 }
148148 func (s *cliSink) ToolRunning(string) {}
@@ -89,8 +89,8 @@ func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stde
89 if req.WithContext {89 if req.WithContext {
90 label += ", with the conversation"90 label += ", with the conversation"
91 }91 }
92- fmt.Fprintln(stderr, label+"…")92+ _, _ = fmt.Fprintln(stderr, label+"…")
93- p.notify = func(line string) { fmt.Fprintln(stderr, line) }93+ p.notify = func(line string) { _, _ = fmt.Fprintln(stderr, line) }
94 // With postAsk hooks the answer is held back until they have run: what94 // With postAsk hooks the answer is held back until they have run: what
95 // stdout carries is what the caller reads, and a hook may replace it.95 // stdout carries is what the caller reads, and a hook may replace it.
96 var held strings.Builder96 var held strings.Builder
@@ -103,10 +103,10 @@ func (p *Pool) CLI(ctx context.Context, req CLIRequest, cwd string, stdout, stde
103 answer := res.Answer103 answer := res.Answer
104 if !streamed {104 if !streamed {
105 answer = res.Text()105 answer = res.Text()
106- fmt.Fprint(stdout, answer)106+ _, _ = fmt.Fprint(stdout, answer)
107 }107 }
108 if answer != "" && !strings.HasSuffix(answer, "\n") {108 if answer != "" && !strings.HasSuffix(answer, "\n") {
109- fmt.Fprintln(stdout)109+ _, _ = fmt.Fprintln(stdout)
110 }110 }
111 if err == nil && res.Answer == "" {111 if err == nil && res.Answer == "" {
112 return errors.New(req.Agent + " gave no answer")112 return errors.New(req.Agent + " gave no answer")
@@ -135,14 +135,14 @@ type cliSink struct {
135 135
136 var _ ui.Sink = (*cliSink)(nil)136 var _ ui.Sink = (*cliSink)(nil)
137 137
138-func (s *cliSink) Text(text string) { fmt.Fprint(s.out, text) }138+func (s *cliSink) Text(text string) { _, _ = fmt.Fprint(s.out, text) }
139 func (s *cliSink) Reasoning(string) {}139 func (s *cliSink) Reasoning(string) {}
140 func (s *cliSink) ToolStart(ev ui.ToolEvent) {140 func (s *cliSink) ToolStart(ev ui.ToolEvent) {
141- fmt.Fprintln(s.log, "🛠️ "+ev.Title)141+ _, _ = fmt.Fprintln(s.log, "🛠️ "+ev.Title)
142 }142 }
143 func (s *cliSink) ToolEnd(_ string, res ui.ToolResult) {143 func (s *cliSink) ToolEnd(_ string, res ui.ToolResult) {
144 if res.Failed {144 if res.Failed {
145- fmt.Fprintln(s.log, " │ failed")145+ _, _ = fmt.Fprintln(s.log, " │ failed")
146 }146 }
147 }147 }
148 func (s *cliSink) ToolRunning(string) {}148 func (s *cliSink) ToolRunning(string) {}
modified internal/compact/compact_test.go +2 -2
@@ -335,9 +335,9 @@ func countSummaries(msgs []*ai.Message) int {
335335 func TestCompactThroughEngineSummarize(t *testing.T) {
336336 g := genkit.Init(context.Background())
337337 var sawTools int
338- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,
338+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
339339 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
340- func(_ context.Context, r *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
340+ func(_ context.Context, r *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
341341 sawTools = len(r.Tools)
342342 return &ai.ModelResponse{
343343 Message: ai.NewModelMessage(ai.NewTextPart("## Goal\nfrom the fake model")),
@@ -335,9 +335,9 @@ func countSummaries(msgs []*ai.Message) int {
335 func TestCompactThroughEngineSummarize(t *testing.T) {335 func TestCompactThroughEngineSummarize(t *testing.T) {
336 g := genkit.Init(context.Background())336 g := genkit.Init(context.Background())
337 var sawTools int337 var sawTools int
338- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,338+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
339 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},339 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
340- func(_ context.Context, r *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {340+ func(_ context.Context, r *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
341 sawTools = len(r.Tools)341 sawTools = len(r.Tools)
342 return &ai.ModelResponse{342 return &ai.ModelResponse{
343 Message: ai.NewModelMessage(ai.NewTextPart("## Goal\nfrom the fake model")),343 Message: ai.NewModelMessage(ai.NewTextPart("## Goal\nfrom the fake model")),
modified internal/engine/engine.go +2 -2
@@ -297,7 +297,7 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a
297297 // Text the broken attempt already streamed is not taken back.
298298 if streamBroken(err) && ctx.Err() == nil {
299299 spinner.Stop()
300- fmt.Fprintf(ui.Out, "\n[%s — retrying this model call once]\n", streamNotice(err))
300+ _, _ = fmt.Fprintf(ui.Out, "\n[%s — retrying this model call once]\n", streamNotice(err))
301301 pl.expect()
302302 resp, err = next(ctx, params)
303303 err = withServerMessage(ctx, err)
@@ -321,7 +321,7 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a
321321 }
322322 // ui.Out: stderr in ACP mode, never the JSON-RPC stream.
323323 spinner.Stop()
324- fmt.Fprintf(ui.Out, "\n[tool call arguments did not parse (%s: …%s) — handing it back to the model, %d/%d]\n",
324+ _, _ = fmt.Fprintf(ui.Out, "\n[tool call arguments did not parse (%s: …%s) — handing it back to the model, %d/%d]\n",
325325 what, lastRunes(err.Error(), 60), argRetries, maxArgRetries)
326326 params.Request.Messages = append(slices.Clone(params.Request.Messages), argsRetryNote(truncated))
327327 full = slices.Clone(params.Request.Messages)
@@ -297,7 +297,7 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a
297 // Text the broken attempt already streamed is not taken back.297 // Text the broken attempt already streamed is not taken back.
298 if streamBroken(err) && ctx.Err() == nil {298 if streamBroken(err) && ctx.Err() == nil {
299 spinner.Stop()299 spinner.Stop()
300- fmt.Fprintf(ui.Out, "\n[%s — retrying this model call once]\n", streamNotice(err))300+ _, _ = fmt.Fprintf(ui.Out, "\n[%s — retrying this model call once]\n", streamNotice(err))
301 pl.expect()301 pl.expect()
302 resp, err = next(ctx, params)302 resp, err = next(ctx, params)
303 err = withServerMessage(ctx, err)303 err = withServerMessage(ctx, err)
@@ -321,7 +321,7 @@ func (e *Engine) generate(ctx context.Context, messages []*ai.Message, tools []a
321 }321 }
322 // ui.Out: stderr in ACP mode, never the JSON-RPC stream.322 // ui.Out: stderr in ACP mode, never the JSON-RPC stream.
323 spinner.Stop()323 spinner.Stop()
324- fmt.Fprintf(ui.Out, "\n[tool call arguments did not parse (%s: …%s) — handing it back to the model, %d/%d]\n",324+ _, _ = fmt.Fprintf(ui.Out, "\n[tool call arguments did not parse (%s: …%s) — handing it back to the model, %d/%d]\n",
325 what, lastRunes(err.Error(), 60), argRetries, maxArgRetries)325 what, lastRunes(err.Error(), 60), argRetries, maxArgRetries)
326 params.Request.Messages = append(slices.Clone(params.Request.Messages), argsRetryNote(truncated))326 params.Request.Messages = append(slices.Clone(params.Request.Messages), argsRetryNote(truncated))
327 full = slices.Clone(params.Request.Messages)327 full = slices.Clone(params.Request.Messages)
modified internal/engine/engine_test.go +4 -4
@@ -32,9 +32,9 @@ func fakeGenkit(t *testing.T, turns ...*ai.ModelResponse) (*genkit.Genkit, *int)
3232 g := genkit.Init(context.Background())
3333
3434 calls := 0
35- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,
35+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
3636 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
37- func(_ context.Context, _ *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
37+ func(_ context.Context, _ *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
3838 if calls >= len(turns) {
3939 t.Errorf("the model was called %d times for %d planned turns", calls+1, len(turns))
4040 return turns[len(turns)-1], nil
@@ -301,9 +301,9 @@ func TestSummarize(t *testing.T) {
301301 var sawTools int
302302 answers := []string{" ## Goal\nnotes\n", ""}
303303 call := 0
304- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,
304+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
305305 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
306- func(_ context.Context, r *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
306+ func(_ context.Context, r *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
307307 sawTools = len(r.Tools)
308308 a := answers[call]
309309 call++
@@ -32,9 +32,9 @@ func fakeGenkit(t *testing.T, turns ...*ai.ModelResponse) (*genkit.Genkit, *int)
32 g := genkit.Init(context.Background())32 g := genkit.Init(context.Background())
33 33
34 calls := 034 calls := 0
35- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,35+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
36 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},36 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
37- func(_ context.Context, _ *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {37+ func(_ context.Context, _ *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
38 if calls >= len(turns) {38 if calls >= len(turns) {
39 t.Errorf("the model was called %d times for %d planned turns", calls+1, len(turns))39 t.Errorf("the model was called %d times for %d planned turns", calls+1, len(turns))
40 return turns[len(turns)-1], nil40 return turns[len(turns)-1], nil
@@ -301,9 +301,9 @@ func TestSummarize(t *testing.T) {
301 var sawTools int301 var sawTools int
302 answers := []string{" ## Goal\nnotes\n", ""}302 answers := []string{" ## Goal\nnotes\n", ""}
303 call := 0303 call := 0
304- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,304+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
305 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},305 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
306- func(_ context.Context, r *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {306+ func(_ context.Context, r *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
307 sawTools = len(r.Tools)307 sawTools = len(r.Tools)
308 a := answers[call]308 a := answers[call]
309 call++309 call++
modified internal/engine/openai_compat.go +2 -2
@@ -268,7 +268,7 @@ func reachable(baseURL string) bool {
268268 if err != nil {
269269 return false
270270 }
271- defer resp.Body.Close()
271+ defer func() { _ = resp.Body.Close() }()
272272 return resp.StatusCode == http.StatusOK
273273 }
274274
@@ -279,7 +279,7 @@ func getJSON(url string, out any) bool {
279279 if err != nil {
280280 return false
281281 }
282- defer resp.Body.Close()
282+ defer func() { _ = resp.Body.Close() }()
283283 if resp.StatusCode != http.StatusOK {
284284 return false
285285 }
@@ -268,7 +268,7 @@ func reachable(baseURL string) bool {
268 if err != nil {268 if err != nil {
269 return false269 return false
270 }270 }
271- defer resp.Body.Close()271+ defer func() { _ = resp.Body.Close() }()
272 return resp.StatusCode == http.StatusOK272 return resp.StatusCode == http.StatusOK
273 }273 }
274 274
@@ -279,7 +279,7 @@ func getJSON(url string, out any) bool {
279 if err != nil {279 if err != nil {
280 return false280 return false
281 }281 }
282- defer resp.Body.Close()282+ defer func() { _ = resp.Body.Close() }()
283 if resp.StatusCode != http.StatusOK {283 if resp.StatusCode != http.StatusOK {
284 return false284 return false
285 }285 }
modified internal/engine/progress.go +1 -1
@@ -146,7 +146,7 @@ func watchdog(ctx context.Context, between, firstToken time.Duration) (genCtx co
146146 spinner.Stop()
147147 // ui.Out, not stdout: in ACP mode this diagnosis belongs to
148148 // the logs (stderr), never to the JSON-RPC stream.
149- fmt.Fprintf(ui.Out, "\n[watchdog: %s after %v]\n", what, limit)
149+ _, _ = fmt.Fprintf(ui.Out, "\n[watchdog: %s after %v]\n", what, limit)
150150 cancel()
151151 return
152152 }
@@ -146,7 +146,7 @@ func watchdog(ctx context.Context, between, firstToken time.Duration) (genCtx co
146 spinner.Stop()146 spinner.Stop()
147 // ui.Out, not stdout: in ACP mode this diagnosis belongs to147 // ui.Out, not stdout: in ACP mode this diagnosis belongs to
148 // the logs (stderr), never to the JSON-RPC stream.148 // the logs (stderr), never to the JSON-RPC stream.
149- fmt.Fprintf(ui.Out, "\n[watchdog: %s after %v]\n", what, limit)149+ _, _ = fmt.Fprintf(ui.Out, "\n[watchdog: %s after %v]\n", what, limit)
150 cancel()150 cancel()
151 return151 return
152 }152 }
modified internal/engine/progress_test.go +6 -6
@@ -47,7 +47,7 @@ func timeouts(t *testing.T, between, first time.Duration) {
4747 func readingServer(t *testing.T, reading, every, pause time.Duration, sawProgress *bool) string {
4848 srv, _ := fakeLlama(t, func(_ int, w http.ResponseWriter) {
4949 fl := w.(http.Flusher)
50- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":null}`, ""))
50+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":null}`, ""))
5151 fl.Flush()
5252 for start := time.Now(); time.Since(start) < reading; {
5353 if every == 0 {
@@ -55,15 +55,15 @@ func readingServer(t *testing.T, reading, every, pause time.Duration, sawProgres
5555 break
5656 }
5757 time.Sleep(every)
58- fmt.Fprint(w, `data: {"choices":[{"index":0,"delta":{"content":null}}],"prompt_progress":{"total":25000,"cache":0,"processed":1000,"time_ms":10}}`+"\n\n")
58+ _, _ = fmt.Fprint(w, `data: {"choices":[{"index":0,"delta":{"content":null}}],"prompt_progress":{"total":25000,"cache":0,"processed":1000,"time_ms":10}}`+"\n\n")
5959 fl.Flush()
6060 }
61- fmt.Fprint(w, sseChunkLine(`{"content":"half"}`, ""))
61+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":"half"}`, ""))
6262 fl.Flush()
6363 time.Sleep(pause)
64- fmt.Fprint(w, sseChunkLine(`{"content":" answer"}`, ""))
65- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
66- fmt.Fprint(w, "data: [DONE]\n\n")
64+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":" answer"}`, ""))
65+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
66+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
6767 })
6868 if sawProgress != nil {
6969 // The provider must ask for it: look at what the next request sends.
@@ -47,7 +47,7 @@ func timeouts(t *testing.T, between, first time.Duration) {
47 func readingServer(t *testing.T, reading, every, pause time.Duration, sawProgress *bool) string {47 func readingServer(t *testing.T, reading, every, pause time.Duration, sawProgress *bool) string {
48 srv, _ := fakeLlama(t, func(_ int, w http.ResponseWriter) {48 srv, _ := fakeLlama(t, func(_ int, w http.ResponseWriter) {
49 fl := w.(http.Flusher)49 fl := w.(http.Flusher)
50- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":null}`, ""))50+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":null}`, ""))
51 fl.Flush()51 fl.Flush()
52 for start := time.Now(); time.Since(start) < reading; {52 for start := time.Now(); time.Since(start) < reading; {
53 if every == 0 {53 if every == 0 {
@@ -55,15 +55,15 @@ func readingServer(t *testing.T, reading, every, pause time.Duration, sawProgres
55 break55 break
56 }56 }
57 time.Sleep(every)57 time.Sleep(every)
58- fmt.Fprint(w, `data: {"choices":[{"index":0,"delta":{"content":null}}],"prompt_progress":{"total":25000,"cache":0,"processed":1000,"time_ms":10}}`+"\n\n")58+ _, _ = fmt.Fprint(w, `data: {"choices":[{"index":0,"delta":{"content":null}}],"prompt_progress":{"total":25000,"cache":0,"processed":1000,"time_ms":10}}`+"\n\n")
59 fl.Flush()59 fl.Flush()
60 }60 }
61- fmt.Fprint(w, sseChunkLine(`{"content":"half"}`, ""))61+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":"half"}`, ""))
62 fl.Flush()62 fl.Flush()
63 time.Sleep(pause)63 time.Sleep(pause)
64- fmt.Fprint(w, sseChunkLine(`{"content":" answer"}`, ""))64+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":" answer"}`, ""))
65- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))65+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
66- fmt.Fprint(w, "data: [DONE]\n\n")66+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
67 })67 })
68 if sawProgress != nil {68 if sawProgress != nil {
69 // The provider must ask for it: look at what the next request sends.69 // The provider must ask for it: look at what the next request sends.
modified internal/engine/reasoning_test.go +9 -9
@@ -110,7 +110,7 @@ func TestReasoningTapIsScopedToItsRequest(t *testing.T) {
110110 t.Errorf("%s: %v", who, err)
111111 return
112112 }
113- defer resp.Body.Close()
113+ defer func() { _ = resp.Body.Close() }()
114114 _, _ = io.ReadAll(resp.Body)
115115 }
116116
@@ -175,10 +175,10 @@ var _ = option.WithMiddleware(reasoningTap) // the middleware still fits the opt
175175 // request.
176176 func TestReasoningStaysOutOfTheHistory(t *testing.T) {
177177 srv, bodies := recordingLlama(t, func(w http.ResponseWriter) {
178- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"let me think"}`, ""))
179- fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))
180- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
181- fmt.Fprint(w, "data: [DONE]\n\n")
178+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"let me think"}`, ""))
179+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))
180+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
181+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
182182 })
183183 e := engineOn(t, srv.URL+"/v1")
184184
@@ -213,13 +213,13 @@ func TestReasoningStreamsAsOneBlock(t *testing.T) {
213213 // the tap reads them all before Genkit's callback runs and the bug
214214 // does not show.
215215 for _, word := range []string{"The", " user", " asks"} {
216- fmt.Fprint(w, sseChunkLine(fmt.Sprintf(`{"reasoning_content":%q}`, word), ""))
216+ _, _ = fmt.Fprint(w, sseChunkLine(fmt.Sprintf(`{"reasoning_content":%q}`, word), ""))
217217 w.(http.Flusher).Flush()
218218 time.Sleep(20 * time.Millisecond)
219219 }
220- fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))
221- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
222- fmt.Fprint(w, "data: [DONE]\n\n")
220+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))
221+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
222+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
223223 })
224224 e := engineOn(t, srv.URL+"/v1")
225225 var out strings.Builder
@@ -110,7 +110,7 @@ func TestReasoningTapIsScopedToItsRequest(t *testing.T) {
110 t.Errorf("%s: %v", who, err)110 t.Errorf("%s: %v", who, err)
111 return111 return
112 }112 }
113- defer resp.Body.Close()113+ defer func() { _ = resp.Body.Close() }()
114 _, _ = io.ReadAll(resp.Body)114 _, _ = io.ReadAll(resp.Body)
115 }115 }
116 116
@@ -175,10 +175,10 @@ var _ = option.WithMiddleware(reasoningTap) // the middleware still fits the opt
175 // request.175 // request.
176 func TestReasoningStaysOutOfTheHistory(t *testing.T) {176 func TestReasoningStaysOutOfTheHistory(t *testing.T) {
177 srv, bodies := recordingLlama(t, func(w http.ResponseWriter) {177 srv, bodies := recordingLlama(t, func(w http.ResponseWriter) {
178- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"let me think"}`, ""))178+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"let me think"}`, ""))
179- fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))179+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))
180- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))180+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
181- fmt.Fprint(w, "data: [DONE]\n\n")181+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
182 })182 })
183 e := engineOn(t, srv.URL+"/v1")183 e := engineOn(t, srv.URL+"/v1")
184 184
@@ -213,13 +213,13 @@ func TestReasoningStreamsAsOneBlock(t *testing.T) {
213 // the tap reads them all before Genkit's callback runs and the bug213 // the tap reads them all before Genkit's callback runs and the bug
214 // does not show.214 // does not show.
215 for _, word := range []string{"The", " user", " asks"} {215 for _, word := range []string{"The", " user", " asks"} {
216- fmt.Fprint(w, sseChunkLine(fmt.Sprintf(`{"reasoning_content":%q}`, word), ""))216+ _, _ = fmt.Fprint(w, sseChunkLine(fmt.Sprintf(`{"reasoning_content":%q}`, word), ""))
217 w.(http.Flusher).Flush()217 w.(http.Flusher).Flush()
218 time.Sleep(20 * time.Millisecond)218 time.Sleep(20 * time.Millisecond)
219 }219 }
220- fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))220+ _, _ = fmt.Fprint(w, sseChunkLine(`{"content":"391"}`, ""))
221- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))221+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
222- fmt.Fprint(w, "data: [DONE]\n\n")222+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
223 })223 })
224 e := engineOn(t, srv.URL+"/v1")224 e := engineOn(t, srv.URL+"/v1")
225 var out strings.Builder225 var out strings.Builder
modified internal/engine/servererr_test.go +7 -7
@@ -38,16 +38,16 @@ func fakeLlama(t *testing.T, answer func(call int, w http.ResponseWriter)) (*htt
3838 }
3939
4040 func goodAnswer(w http.ResponseWriter) {
41- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"all good"}`, ""))
42- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
43- fmt.Fprint(w, "data: [DONE]\n\n")
41+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"all good"}`, ""))
42+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
43+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
4444 }
4545
4646 // llama.cpp's way of failing mid-stream: an `error:` event, after 200 and
4747 // some content.
4848 func errorEvent(w http.ResponseWriter) {
49- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))
50- fmt.Fprint(w, `error: {"code":500,"message":"Failed to parse input at pos 12","type":"server_error"}`+"\n\n")
49+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))
50+ _, _ = fmt.Fprint(w, `error: {"code":500,"message":"Failed to parse input at pos 12","type":"server_error"}`+"\n\n")
5151 }
5252
5353 func engineOn(t *testing.T, baseURL string) *Engine {
@@ -101,10 +101,10 @@ func TestServerErrorEventIsExplained(t *testing.T) {
101101 // line that says what happened.
102102 func TestCutStreamIsExplained(t *testing.T) {
103103 srv, _ := fakeLlama(t, func(_ int, w http.ResponseWriter) {
104- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))
104+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))
105105 w.(http.Flusher).Flush()
106106 conn, _, _ := w.(http.Hijacker).Hijack()
107- conn.Close()
107+ _ = conn.Close()
108108 })
109109 e := engineOn(t, srv.URL+"/v1")
110110 _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("hi")}, nil)
@@ -38,16 +38,16 @@ func fakeLlama(t *testing.T, answer func(call int, w http.ResponseWriter)) (*htt
38 }38 }
39 39
40 func goodAnswer(w http.ResponseWriter) {40 func goodAnswer(w http.ResponseWriter) {
41- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"all good"}`, ""))41+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"all good"}`, ""))
42- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))42+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
43- fmt.Fprint(w, "data: [DONE]\n\n")43+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
44 }44 }
45 45
46 // llama.cpp's way of failing mid-stream: an `error:` event, after 200 and46 // llama.cpp's way of failing mid-stream: an `error:` event, after 200 and
47 // some content.47 // some content.
48 func errorEvent(w http.ResponseWriter) {48 func errorEvent(w http.ResponseWriter) {
49- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))49+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))
50- fmt.Fprint(w, `error: {"code":500,"message":"Failed to parse input at pos 12","type":"server_error"}`+"\n\n")50+ _, _ = fmt.Fprint(w, `error: {"code":500,"message":"Failed to parse input at pos 12","type":"server_error"}`+"\n\n")
51 }51 }
52 52
53 func engineOn(t *testing.T, baseURL string) *Engine {53 func engineOn(t *testing.T, baseURL string) *Engine {
@@ -101,10 +101,10 @@ func TestServerErrorEventIsExplained(t *testing.T) {
101 // line that says what happened.101 // line that says what happened.
102 func TestCutStreamIsExplained(t *testing.T) {102 func TestCutStreamIsExplained(t *testing.T) {
103 srv, _ := fakeLlama(t, func(_ int, w http.ResponseWriter) {103 srv, _ := fakeLlama(t, func(_ int, w http.ResponseWriter) {
104- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))104+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"Hel"}`, ""))
105 w.(http.Flusher).Flush()105 w.(http.Flusher).Flush()
106 conn, _, _ := w.(http.Hijacker).Hijack()106 conn, _, _ := w.(http.Hijacker).Hijack()
107- conn.Close()107+ _ = conn.Close()
108 })108 })
109 e := engineOn(t, srv.URL+"/v1")109 e := engineOn(t, srv.URL+"/v1")
110 _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("hi")}, nil)110 _, _, err := e.Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("hi")}, nil)
modified internal/engine/structured_test.go +6 -6
@@ -31,9 +31,9 @@ func withOutputSchema(t *testing.T, src string) {
3131 // answering streams content as the model's whole answer.
3232 func answering(content string) func(http.ResponseWriter) {
3333 return func(w http.ResponseWriter) {
34- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":`+strconv.Quote(content)+`}`, ""))
35- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
36- fmt.Fprint(w, "data: [DONE]\n\n")
34+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":`+strconv.Quote(content)+`}`, ""))
35+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
36+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
3737 }
3838 }
3939
@@ -140,9 +140,9 @@ func TestPropertiesReachTheServerInDeclarationOrder(t *testing.T) {
140140 func TestACutStructuredAnswerNamesMaxTokens(t *testing.T) {
141141 withOutputSchema(t, "var Names []string")
142142 srv, _ := recordingLlama(t, func(w http.ResponseWriter) {
143- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"[\"Ada\",\"Gra"}`, ""))
144- fmt.Fprint(w, sseChunkLine(`{}`, "length"))
145- fmt.Fprint(w, "data: [DONE]\n\n")
143+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"[\"Ada\",\"Gra"}`, ""))
144+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "length"))
145+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
146146 })
147147 _, _, err := engineOn(t, srv.URL+"/v1").Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("?")}, nil)
148148 if err == nil || !strings.Contains(err.Error(), "cut off by max_tokens") {
@@ -31,9 +31,9 @@ func withOutputSchema(t *testing.T, src string) {
31 // answering streams content as the model's whole answer.31 // answering streams content as the model's whole answer.
32 func answering(content string) func(http.ResponseWriter) {32 func answering(content string) func(http.ResponseWriter) {
33 return func(w http.ResponseWriter) {33 return func(w http.ResponseWriter) {
34- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":`+strconv.Quote(content)+`}`, ""))34+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":`+strconv.Quote(content)+`}`, ""))
35- fmt.Fprint(w, sseChunkLine(`{}`, "stop"))35+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "stop"))
36- fmt.Fprint(w, "data: [DONE]\n\n")36+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
37 }37 }
38 }38 }
39 39
@@ -140,9 +140,9 @@ func TestPropertiesReachTheServerInDeclarationOrder(t *testing.T) {
140 func TestACutStructuredAnswerNamesMaxTokens(t *testing.T) {140 func TestACutStructuredAnswerNamesMaxTokens(t *testing.T) {
141 withOutputSchema(t, "var Names []string")141 withOutputSchema(t, "var Names []string")
142 srv, _ := recordingLlama(t, func(w http.ResponseWriter) {142 srv, _ := recordingLlama(t, func(w http.ResponseWriter) {
143- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"[\"Ada\",\"Gra"}`, ""))143+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","content":"[\"Ada\",\"Gra"}`, ""))
144- fmt.Fprint(w, sseChunkLine(`{}`, "length"))144+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "length"))
145- fmt.Fprint(w, "data: [DONE]\n\n")145+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
146 })146 })
147 _, _, err := engineOn(t, srv.URL+"/v1").Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("?")}, nil)147 _, _, err := engineOn(t, srv.URL+"/v1").Generate(context.Background(), []*ai.Message{ai.NewUserTextMessage("?")}, nil)
148 if err == nil || !strings.Contains(err.Error(), "cut off by max_tokens") {148 if err == nil || !strings.Contains(err.Error(), "cut off by max_tokens") {
modified internal/engine/summary_test.go +3 -3
@@ -106,9 +106,9 @@ func TestSummaryTurnsReasoningOffOverSampling(t *testing.T) {
106106 // and names the setting — the session file only ever said "empty summary".
107107 func TestEmptySummarySaysWhy(t *testing.T) {
108108 srv, _ := recordingLlama(t, func(w http.ResponseWriter) {
109- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"Let me think about the session..."}`, ""))
110- fmt.Fprint(w, sseChunkLine(`{}`, "length"))
111- fmt.Fprint(w, "data: [DONE]\n\n")
109+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"Let me think about the session..."}`, ""))
110+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "length"))
111+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
112112 })
113113 e := engineOn(t, srv.URL+"/v1")
114114
@@ -106,9 +106,9 @@ func TestSummaryTurnsReasoningOffOverSampling(t *testing.T) {
106 // and names the setting — the session file only ever said "empty summary".106 // and names the setting — the session file only ever said "empty summary".
107 func TestEmptySummarySaysWhy(t *testing.T) {107 func TestEmptySummarySaysWhy(t *testing.T) {
108 srv, _ := recordingLlama(t, func(w http.ResponseWriter) {108 srv, _ := recordingLlama(t, func(w http.ResponseWriter) {
109- fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"Let me think about the session..."}`, ""))109+ _, _ = fmt.Fprint(w, sseChunkLine(`{"role":"assistant","reasoning_content":"Let me think about the session..."}`, ""))
110- fmt.Fprint(w, sseChunkLine(`{}`, "length"))110+ _, _ = fmt.Fprint(w, sseChunkLine(`{}`, "length"))
111- fmt.Fprint(w, "data: [DONE]\n\n")111+ _, _ = fmt.Fprint(w, "data: [DONE]\n\n")
112 })112 })
113 e := engineOn(t, srv.URL+"/v1")113 e := engineOn(t, srv.URL+"/v1")
114 114
modified internal/engine/thoughts.go +3 -3
@@ -41,10 +41,10 @@ func (t *thoughts) emit(text string) {
4141 // without it the marker lands on the spinner's line and the next
4242 // frame erases it.
4343 spinner.Stop()
44- fmt.Fprint(ui.Out, "\n💭 ")
44+ _, _ = fmt.Fprint(ui.Out, "\n💭 ")
4545 t.open = true
4646 }
47- fmt.Fprint(ui.Out, dim(text))
47+ _, _ = fmt.Fprint(ui.Out, dim(text))
4848 }
4949
5050 // done ends the block: a newline, so the answer starts on a line of its own.
@@ -60,7 +60,7 @@ func (t *thoughts) done() {
6060 return
6161 }
6262 t.open = false
63- fmt.Fprintln(ui.Out)
63+ _, _ = fmt.Fprintln(ui.Out)
6464 }
6565
6666 // dim renders reasoning as dim italic — a different colour and a different
@@ -41,10 +41,10 @@ func (t *thoughts) emit(text string) {
41 // without it the marker lands on the spinner's line and the next41 // without it the marker lands on the spinner's line and the next
42 // frame erases it.42 // frame erases it.
43 spinner.Stop()43 spinner.Stop()
44- fmt.Fprint(ui.Out, "\n💭 ")44+ _, _ = fmt.Fprint(ui.Out, "\n💭 ")
45 t.open = true45 t.open = true
46 }46 }
47- fmt.Fprint(ui.Out, dim(text))47+ _, _ = fmt.Fprint(ui.Out, dim(text))
48 }48 }
49 49
50 // done ends the block: a newline, so the answer starts on a line of its own.50 // done ends the block: a newline, so the answer starts on a line of its own.
@@ -60,7 +60,7 @@ func (t *thoughts) done() {
60 return60 return
61 }61 }
62 t.open = false62 t.open = false
63- fmt.Fprintln(ui.Out)63+ _, _ = fmt.Fprintln(ui.Out)
64 }64 }
65 65
66 // dim renders reasoning as dim italic — a different colour and a different66 // dim renders reasoning as dim italic — a different colour and a different
modified internal/engine/toolargs_test.go +7 -7
@@ -14,7 +14,7 @@ import (
1414
1515 // The plugin's error for a tool call cut off by max_tokens, as it was seen on
1616 // 2026-09-26 (shortened: the real one quoted the whole document).
17-var truncatedArgs = errors.New(`could not parse tool args: unmarshal failed to parse json string {"path":"docs/M.md","content":"# Mémoire: unexpected end of JSON input`)
17+var errTruncatedArgs = errors.New(`could not parse tool args: unmarshal failed to parse json string {"path":"docs/M.md","content":"# Mémoire: unexpected end of JSON input`)
1818
1919 // scriptedGenkit registers a model that plays `steps` in order — a response
2020 // or an error — and records every request it was sent, plus the fake `bash`.
@@ -22,9 +22,9 @@ func scriptedGenkit(t *testing.T, steps ...any) (*genkit.Genkit, *[]*ai.ModelReq
2222 t.Helper()
2323 g := genkit.Init(context.Background())
2424 var seen []*ai.ModelRequest
25- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,
25+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
2626 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
27- func(_ context.Context, req *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
27+ func(_ context.Context, req *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
2828 seen = append(seen, req)
2929 if len(seen) > len(steps) {
3030 t.Fatalf("the model was called %d times for %d planned steps", len(seen), len(steps))
@@ -51,7 +51,7 @@ func scriptedGenkit(t *testing.T, steps ...any) (*genkit.Genkit, *[]*ai.ModelReq
5151 // front end keeps, where the model will read it again for the next piece.
5252 func TestTruncatedToolCallIsHandedBack(t *testing.T) {
5353 bash := modelTurn(ai.NewToolRequestPart(&ai.ToolRequest{Name: "bash", Input: map[string]any{"command": "ls"}, Ref: "1"}))
54- g, seen := scriptedGenkit(t, bash, truncatedArgs, modelTurn(ai.NewTextPart("written in two parts")))
54+ g, seen := scriptedGenkit(t, bash, errTruncatedArgs, modelTurn(ai.NewTextPart("written in two parts")))
5555
5656 resp, full, err := testEngine(g).Generate(context.Background(),
5757 []*ai.Message{ai.NewUserTextMessage("write the document")},
@@ -92,9 +92,9 @@ func TestTruncatedToolCallGivesUpAndExplains(t *testing.T) {
9292 config.Cfg.Sampling = map[string]any{"max_tokens": 4096}
9393 defer func() { config.Cfg.Sampling = old }()
9494
95- steps := []any{truncatedArgs}
95+ steps := []any{errTruncatedArgs}
9696 for range maxArgRetries {
97- steps = append(steps, truncatedArgs)
97+ steps = append(steps, errTruncatedArgs)
9898 }
9999 g, seen := scriptedGenkit(t, steps...)
100100 e := testEngine(g)
@@ -120,7 +120,7 @@ func TestArgsError(t *testing.T) {
120120 }{
121121 {nil, false, false},
122122 {errors.New("connection refused"), false, false},
123- {truncatedArgs, true, false},
123+ {errTruncatedArgs, true, false},
124124 {errors.New(`could not parse tool args: unmarshal failed to parse json string {"a":1}}: invalid character '}'`), false, true},
125125 }
126126 for _, c := range cases {
@@ -14,7 +14,7 @@ import (
14 14
15 // The plugin's error for a tool call cut off by max_tokens, as it was seen on15 // The plugin's error for a tool call cut off by max_tokens, as it was seen on
16 // 2026-09-26 (shortened: the real one quoted the whole document).16 // 2026-09-26 (shortened: the real one quoted the whole document).
17-var truncatedArgs = errors.New(`could not parse tool args: unmarshal failed to parse json string {"path":"docs/M.md","content":"# Mémoire: unexpected end of JSON input`)17+var errTruncatedArgs = errors.New(`could not parse tool args: unmarshal failed to parse json string {"path":"docs/M.md","content":"# Mémoire: unexpected end of JSON input`)
18 18
19 // scriptedGenkit registers a model that plays `steps` in order — a response19 // scriptedGenkit registers a model that plays `steps` in order — a response
20 // or an error — and records every request it was sent, plus the fake `bash`.20 // or an error — and records every request it was sent, plus the fake `bash`.
@@ -22,9 +22,9 @@ func scriptedGenkit(t *testing.T, steps ...any) (*genkit.Genkit, *[]*ai.ModelReq
22 t.Helper()22 t.Helper()
23 g := genkit.Init(context.Background())23 g := genkit.Init(context.Background())
24 var seen []*ai.ModelRequest24 var seen []*ai.ModelRequest
25- genkit.DefineModel(g, "dmr/"+config.Cfg.Model,25+ genkit.DefineModelAction(g, "dmr/"+config.Cfg.Model,
26 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},26 &ai.ModelOptions{Supports: &ai.ModelSupports{Tools: true, Multiturn: true, SystemRole: true}},
27- func(_ context.Context, req *ai.ModelRequest, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {27+ func(_ context.Context, req *ai.ModelRequest, _ any, _ ai.ModelStreamCallback) (*ai.ModelResponse, error) {
28 seen = append(seen, req)28 seen = append(seen, req)
29 if len(seen) > len(steps) {29 if len(seen) > len(steps) {
30 t.Fatalf("the model was called %d times for %d planned steps", len(seen), len(steps))30 t.Fatalf("the model was called %d times for %d planned steps", len(seen), len(steps))
@@ -51,7 +51,7 @@ func scriptedGenkit(t *testing.T, steps ...any) (*genkit.Genkit, *[]*ai.ModelReq
51 // front end keeps, where the model will read it again for the next piece.51 // front end keeps, where the model will read it again for the next piece.
52 func TestTruncatedToolCallIsHandedBack(t *testing.T) {52 func TestTruncatedToolCallIsHandedBack(t *testing.T) {
53 bash := modelTurn(ai.NewToolRequestPart(&ai.ToolRequest{Name: "bash", Input: map[string]any{"command": "ls"}, Ref: "1"}))53 bash := modelTurn(ai.NewToolRequestPart(&ai.ToolRequest{Name: "bash", Input: map[string]any{"command": "ls"}, Ref: "1"}))
54- g, seen := scriptedGenkit(t, bash, truncatedArgs, modelTurn(ai.NewTextPart("written in two parts")))54+ g, seen := scriptedGenkit(t, bash, errTruncatedArgs, modelTurn(ai.NewTextPart("written in two parts")))
55 55
56 resp, full, err := testEngine(g).Generate(context.Background(),56 resp, full, err := testEngine(g).Generate(context.Background(),
57 []*ai.Message{ai.NewUserTextMessage("write the document")},57 []*ai.Message{ai.NewUserTextMessage("write the document")},
@@ -92,9 +92,9 @@ func TestTruncatedToolCallGivesUpAndExplains(t *testing.T) {
92 config.Cfg.Sampling = map[string]any{"max_tokens": 4096}92 config.Cfg.Sampling = map[string]any{"max_tokens": 4096}
93 defer func() { config.Cfg.Sampling = old }()93 defer func() { config.Cfg.Sampling = old }()
94 94
95- steps := []any{truncatedArgs}95+ steps := []any{errTruncatedArgs}
96 for range maxArgRetries {96 for range maxArgRetries {
97- steps = append(steps, truncatedArgs)97+ steps = append(steps, errTruncatedArgs)
98 }98 }
99 g, seen := scriptedGenkit(t, steps...)99 g, seen := scriptedGenkit(t, steps...)
100 e := testEngine(g)100 e := testEngine(g)
@@ -120,7 +120,7 @@ func TestArgsError(t *testing.T) {
120 }{120 }{
121 {nil, false, false},121 {nil, false, false},
122 {errors.New("connection refused"), false, false},122 {errors.New("connection refused"), false, false},
123- {truncatedArgs, true, false},123+ {errTruncatedArgs, true, false},
124 {errors.New(`could not parse tool args: unmarshal failed to parse json string {"a":1}}: invalid character '}'`), false, true},124 {errors.New(`could not parse tool args: unmarshal failed to parse json string {"a":1}}: invalid character '}'`), false, true},
125 }125 }
126 for _, c := range cases {126 for _, c := range cases {
modified internal/engine/trace.go +3 -1
@@ -51,7 +51,9 @@ func traceMiddleware(dir, agent string) option.Middleware {
5151 where := traceDirOf(dir)
5252 base := filepath.Join(where, fmt.Sprintf("%s-%s-%04d", time.Now().Format("20060102-150405.000"), agent, traceSeq.Add(1)))
5353 if err := os.MkdirAll(where, 0o755); err != nil {
54- traceWarned.Do(func() { fmt.Fprintf(ui.Out, "[trace: cannot write to %s: %v — nothing is traced]\n", where, err) })
54+ traceWarned.Do(func() {
55+ _, _ = fmt.Fprintf(ui.Out, "[trace: cannot write to %s: %v — nothing is traced]\n", where, err)
56+ })
5557 }
5658 if req.Body != nil {
5759 if body, err := io.ReadAll(req.Body); err == nil {
@@ -51,7 +51,9 @@ func traceMiddleware(dir, agent string) option.Middleware {
51 where := traceDirOf(dir)51 where := traceDirOf(dir)
52 base := filepath.Join(where, fmt.Sprintf("%s-%s-%04d", time.Now().Format("20060102-150405.000"), agent, traceSeq.Add(1)))52 base := filepath.Join(where, fmt.Sprintf("%s-%s-%04d", time.Now().Format("20060102-150405.000"), agent, traceSeq.Add(1)))
53 if err := os.MkdirAll(where, 0o755); err != nil {53 if err := os.MkdirAll(where, 0o755); err != nil {
54- traceWarned.Do(func() { fmt.Fprintf(ui.Out, "[trace: cannot write to %s: %v — nothing is traced]\n", where, err) })54+ traceWarned.Do(func() {
55+ _, _ = fmt.Fprintf(ui.Out, "[trace: cannot write to %s: %v — nothing is traced]\n", where, err)
56+ })
55 }57 }
56 if req.Body != nil {58 if req.Body != nil {
57 if body, err := io.ReadAll(req.Body); err == nil {59 if body, err := io.ReadAll(req.Body); err == nil {
modified internal/eval/routing_test.go +1 -1
@@ -116,7 +116,7 @@ func TestSkillRouting(t *testing.T) {
116116 if devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0); err == nil {
117117 saved := os.Stdout
118118 os.Stdout = devnull
119- t.Cleanup(func() { os.Stdout = saved; devnull.Close() })
119+ t.Cleanup(func() { os.Stdout = saved; _ = devnull.Close() })
120120 }
121121
122122 path, err := config.Load(os.Getenv("AGENT_CONFIG"))
@@ -116,7 +116,7 @@ func TestSkillRouting(t *testing.T) {
116 if devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0); err == nil {116 if devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0); err == nil {
117 saved := os.Stdout117 saved := os.Stdout
118 os.Stdout = devnull118 os.Stdout = devnull
119- t.Cleanup(func() { os.Stdout = saved; devnull.Close() })119+ t.Cleanup(func() { os.Stdout = saved; _ = devnull.Close() })
120 }120 }
121 121
122 path, err := config.Load(os.Getenv("AGENT_CONFIG"))122 path, err := config.Load(os.Getenv("AGENT_CONFIG"))
modified internal/fileedit/fileedit_test.go +5 -2
@@ -296,8 +296,11 @@ func TestUnifiedMatchesGitApply(t *testing.T) {
296296 }
297297 }
298298 run("init", "-q")
299- os.WriteFile(filepath.Join(dir, "f.txt"), []byte(tc.old), 0o644)
300- os.WriteFile(filepath.Join(dir, "p.diff"), []byte(patch), 0o644)
299+ for name, body := range map[string]string{"f.txt": tc.old, "p.diff": patch} {
300+ if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
301+ t.Fatal(err)
302+ }
303+ }
301304 run("apply", "p.diff")
302305 if got := fileOf(t, filepath.Join(dir, "f.txt")); got != tc.new {
303306 t.Errorf("after apply: %q, want %q\npatch:\n%s", got, tc.new, patch)
@@ -296,8 +296,11 @@ func TestUnifiedMatchesGitApply(t *testing.T) {
296 }296 }
297 }297 }
298 run("init", "-q")298 run("init", "-q")
299- os.WriteFile(filepath.Join(dir, "f.txt"), []byte(tc.old), 0o644)299+ for name, body := range map[string]string{"f.txt": tc.old, "p.diff": patch} {
300- os.WriteFile(filepath.Join(dir, "p.diff"), []byte(patch), 0o644)300+ if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o644); err != nil {
301+ t.Fatal(err)
302+ }
303+ }
301 run("apply", "p.diff")304 run("apply", "p.diff")
302 if got := fileOf(t, filepath.Join(dir, "f.txt")); got != tc.new {305 if got := fileOf(t, filepath.Join(dir, "f.txt")); got != tc.new {
303 t.Errorf("after apply: %q, want %q\npatch:\n%s", got, tc.new, patch)306 t.Errorf("after apply: %q, want %q\npatch:\n%s", got, tc.new, patch)
modified internal/fileedit/textfile.go +2 -2
@@ -100,10 +100,10 @@ func (f *file) write(body string) error {
100100 if err != nil {
101101 return err
102102 }
103- defer os.Remove(tmp.Name()) // no effect once the rename succeeded
103+ defer func() { _ = os.Remove(tmp.Name()) }() // no effect once the rename succeeded
104104
105105 if _, err := tmp.WriteString(f.render(body)); err != nil {
106- tmp.Close()
106+ _ = tmp.Close() // the write error is the one worth reporting
107107 return err
108108 }
109109 if err := tmp.Close(); err != nil {
@@ -100,10 +100,10 @@ func (f *file) write(body string) error {
100 if err != nil {100 if err != nil {
101 return err101 return err
102 }102 }
103- defer os.Remove(tmp.Name()) // no effect once the rename succeeded103+ defer func() { _ = os.Remove(tmp.Name()) }() // no effect once the rename succeeded
104 104
105 if _, err := tmp.WriteString(f.render(body)); err != nil {105 if _, err := tmp.WriteString(f.render(body)); err != nil {
106- tmp.Close()106+ _ = tmp.Close() // the write error is the one worth reporting
107 return err107 return err
108 }108 }
109 if err := tmp.Close(); err != nil {109 if err := tmp.Close(); err != nil {
modified internal/lsp/format.go +3 -3
@@ -82,7 +82,7 @@ func hoverText(c protocol.HoverContents) string {
8282 }
8383 case protocol.String:
8484 text = string(v)
85- case *protocol.MarkedStringWithLanguage:
85+ case *protocol.MarkedStringWithLanguage: //nolint:staticcheck // SA1019: older servers still answer with MarkedString; the protocol keeps it for them
8686 if v != nil {
8787 text = v.Value
8888 }
@@ -97,11 +97,11 @@ func hoverText(c protocol.HoverContents) string {
9797 }
9898
9999 // markedString is one arm of the deprecated MarkedString union.
100-func markedString(m protocol.MarkedString) string {
100+func markedString(m protocol.MarkedString) string { //nolint:staticcheck // SA1019: older servers still answer with MarkedString; the protocol keeps it for them
101101 switch v := m.(type) {
102102 case protocol.String:
103103 return string(v)
104- case *protocol.MarkedStringWithLanguage:
104+ case *protocol.MarkedStringWithLanguage: //nolint:staticcheck // SA1019: older servers still answer with MarkedString; the protocol keeps it for them
105105 if v != nil {
106106 return v.Value
107107 }
@@ -82,7 +82,7 @@ func hoverText(c protocol.HoverContents) string {
82 }82 }
83 case protocol.String:83 case protocol.String:
84 text = string(v)84 text = string(v)
85- case *protocol.MarkedStringWithLanguage:85+ case *protocol.MarkedStringWithLanguage: //nolint:staticcheck // SA1019: older servers still answer with MarkedString; the protocol keeps it for them
86 if v != nil {86 if v != nil {
87 text = v.Value87 text = v.Value
88 }88 }
@@ -97,11 +97,11 @@ func hoverText(c protocol.HoverContents) string {
97 }97 }
98 98
99 // markedString is one arm of the deprecated MarkedString union.99 // markedString is one arm of the deprecated MarkedString union.
100-func markedString(m protocol.MarkedString) string {100+func markedString(m protocol.MarkedString) string { //nolint:staticcheck // SA1019: older servers still answer with MarkedString; the protocol keeps it for them
101 switch v := m.(type) {101 switch v := m.(type) {
102 case protocol.String:102 case protocol.String:
103 return string(v)103 return string(v)
104- case *protocol.MarkedStringWithLanguage:104+ case *protocol.MarkedStringWithLanguage: //nolint:staticcheck // SA1019: older servers still answer with MarkedString; the protocol keeps it for them
105 if v != nil {105 if v != nil {
106 return v.Value106 return v.Value
107 }107 }
modified internal/mcp/client.go +2 -2
@@ -188,7 +188,7 @@ func ConnectAll(ctx context.Context, servers []config.MCPServer) (map[string]*Se
188188 // Perform discovery
189189 tools, err := client.ListTools(ctx)
190190 if err != nil {
191- client.Close()
191+ _ = client.Close()
192192 warnings = append(warnings, fmt.Sprintf("mcp server %s: discovery failed: %v", server.Name, err))
193193 continue
194194 }
@@ -230,6 +230,6 @@ func ConnectAll(ctx context.Context, servers []config.MCPServer) (map[string]*Se
230230 // CloseAll closes all clients.
231231 func CloseAll(clients map[string]*ServerClient) {
232232 for _, sc := range clients {
233- sc.Close()
233+ _ = sc.Close()
234234 }
235235 }
@@ -188,7 +188,7 @@ func ConnectAll(ctx context.Context, servers []config.MCPServer) (map[string]*Se
188 // Perform discovery188 // Perform discovery
189 tools, err := client.ListTools(ctx)189 tools, err := client.ListTools(ctx)
190 if err != nil {190 if err != nil {
191- client.Close()191+ _ = client.Close()
192 warnings = append(warnings, fmt.Sprintf("mcp server %s: discovery failed: %v", server.Name, err))192 warnings = append(warnings, fmt.Sprintf("mcp server %s: discovery failed: %v", server.Name, err))
193 continue193 continue
194 }194 }
@@ -230,6 +230,6 @@ func ConnectAll(ctx context.Context, servers []config.MCPServer) (map[string]*Se
230 // CloseAll closes all clients.230 // CloseAll closes all clients.
231 func CloseAll(clients map[string]*ServerClient) {231 func CloseAll(clients map[string]*ServerClient) {
232 for _, sc := range clients {232 for _, sc := range clients {
233- sc.Close()233+ _ = sc.Close()
234 }234 }
235 }235 }
modified internal/mcp/http_client.go +3 -3
@@ -170,7 +170,7 @@ func (c *HTTPClient) CallTool(ctx context.Context, toolName string, args map[str
170170 func parseCallResult(raw json.RawMessage) (CallToolResult, error) {
171171 var many callToolResponse
172172 if err := json.Unmarshal(raw, &many); err == nil {
173- return CallToolResult{Content: many.Content, IsError: many.IsError}, nil
173+ return CallToolResult(many), nil
174174 }
175175 var single struct {
176176 Content Content `json:"content"`
@@ -252,7 +252,7 @@ func (c *HTTPClient) request(ctx context.Context, method string, params any) (*m
252252 if err != nil {
253253 return nil, err
254254 }
255- defer httpResp.Body.Close()
255+ defer func() { _ = httpResp.Body.Close() }()
256256
257257 resp, err := readResponse(httpResp, id)
258258 if err != nil {
@@ -339,7 +339,7 @@ func (c *HTTPClient) Close() error {
339339 }
340340 _ = c.setHeaders(req)
341341 if resp, err := c.httpClient.Do(req); err == nil {
342- resp.Body.Close()
342+ _ = resp.Body.Close()
343343 }
344344 c.resetSession()
345345 return nil
@@ -170,7 +170,7 @@ func (c *HTTPClient) CallTool(ctx context.Context, toolName string, args map[str
170 func parseCallResult(raw json.RawMessage) (CallToolResult, error) {170 func parseCallResult(raw json.RawMessage) (CallToolResult, error) {
171 var many callToolResponse171 var many callToolResponse
172 if err := json.Unmarshal(raw, &many); err == nil {172 if err := json.Unmarshal(raw, &many); err == nil {
173- return CallToolResult{Content: many.Content, IsError: many.IsError}, nil173+ return CallToolResult(many), nil
174 }174 }
175 var single struct {175 var single struct {
176 Content Content `json:"content"`176 Content Content `json:"content"`
@@ -252,7 +252,7 @@ func (c *HTTPClient) request(ctx context.Context, method string, params any) (*m
252 if err != nil {252 if err != nil {
253 return nil, err253 return nil, err
254 }254 }
255- defer httpResp.Body.Close()255+ defer func() { _ = httpResp.Body.Close() }()
256 256
257 resp, err := readResponse(httpResp, id)257 resp, err := readResponse(httpResp, id)
258 if err != nil {258 if err != nil {
@@ -339,7 +339,7 @@ func (c *HTTPClient) Close() error {
339 }339 }
340 _ = c.setHeaders(req)340 _ = c.setHeaders(req)
341 if resp, err := c.httpClient.Do(req); err == nil {341 if resp, err := c.httpClient.Do(req); err == nil {
342- resp.Body.Close()342+ _ = resp.Body.Close()
343 }343 }
344 c.resetSession()344 c.resetSession()
345 return nil345 return nil
modified internal/mcp/http_client_test.go +4 -4
@@ -97,8 +97,8 @@ func (s *statefulServer) reply(w http.ResponseWriter, id int, result, errObj str
9797 }
9898 w.Header().Set("Content-Type", "text/event-stream")
9999 // A server notification on the same stream must not be taken for the answer.
100- fmt.Fprint(w, "event: message\ndata: {\"jsonrpc\":\"2.0\",\"method\":\"notifications/message\",\"params\":{\"level\":\"info\"}}\n\n")
101- fmt.Fprintf(w, "event: message\ndata: %s\n\n", body)
100+ _, _ = fmt.Fprint(w, "event: message\ndata: {\"jsonrpc\":\"2.0\",\"method\":\"notifications/message\",\"params\":{\"level\":\"info\"}}\n\n")
101+ _, _ = fmt.Fprintf(w, "event: message\ndata: %s\n\n", body)
102102 }
103103
104104 func newTestClient(t *testing.T, url string) *HTTPClient {
@@ -198,10 +198,10 @@ func TestHTTPClientFallsBackWithoutInitialize(t *testing.T) {
198198 _ = json.NewDecoder(r.Body).Decode(&msg)
199199 w.Header().Set("Content-Type", "application/json")
200200 if msg.Method == "initialize" {
201- fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"error":{"code":-32601,"message":"method not found"}}`, msg.ID)
201+ _, _ = fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"error":{"code":-32601,"message":"method not found"}}`, msg.ID)
202202 return
203203 }
204- fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"result":{"tools":[{"name":"a"}]}}`, msg.ID)
204+ _, _ = fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"result":{"tools":[{"name":"a"}]}}`, msg.ID)
205205 }))
206206 defer ts.Close()
207207
@@ -97,8 +97,8 @@ func (s *statefulServer) reply(w http.ResponseWriter, id int, result, errObj str
97 }97 }
98 w.Header().Set("Content-Type", "text/event-stream")98 w.Header().Set("Content-Type", "text/event-stream")
99 // A server notification on the same stream must not be taken for the answer.99 // A server notification on the same stream must not be taken for the answer.
100- fmt.Fprint(w, "event: message\ndata: {\"jsonrpc\":\"2.0\",\"method\":\"notifications/message\",\"params\":{\"level\":\"info\"}}\n\n")100+ _, _ = fmt.Fprint(w, "event: message\ndata: {\"jsonrpc\":\"2.0\",\"method\":\"notifications/message\",\"params\":{\"level\":\"info\"}}\n\n")
101- fmt.Fprintf(w, "event: message\ndata: %s\n\n", body)101+ _, _ = fmt.Fprintf(w, "event: message\ndata: %s\n\n", body)
102 }102 }
103 103
104 func newTestClient(t *testing.T, url string) *HTTPClient {104 func newTestClient(t *testing.T, url string) *HTTPClient {
@@ -198,10 +198,10 @@ func TestHTTPClientFallsBackWithoutInitialize(t *testing.T) {
198 _ = json.NewDecoder(r.Body).Decode(&msg)198 _ = json.NewDecoder(r.Body).Decode(&msg)
199 w.Header().Set("Content-Type", "application/json")199 w.Header().Set("Content-Type", "application/json")
200 if msg.Method == "initialize" {200 if msg.Method == "initialize" {
201- fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"error":{"code":-32601,"message":"method not found"}}`, msg.ID)201+ _, _ = fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"error":{"code":-32601,"message":"method not found"}}`, msg.ID)
202 return202 return
203 }203 }
204- fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"result":{"tools":[{"name":"a"}]}}`, msg.ID)204+ _, _ = fmt.Fprintf(w, `{"jsonrpc":"2.0","id":%d,"result":{"tools":[{"name":"a"}]}}`, msg.ID)
205 }))205 }))
206 defer ts.Close()206 defer ts.Close()
207 207
modified internal/mcp/http_wire.go +1 -1
@@ -21,7 +21,7 @@ func checkStatus(resp *http.Response) (*http.Response, error) {
2121 if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
2222 return resp, nil
2323 }
24- defer resp.Body.Close()
24+ defer func() { _ = resp.Body.Close() }()
2525 snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
2626 return nil, &httpStatusError{Status: resp.StatusCode, Body: strings.TrimSpace(string(snippet))}
2727 }
@@ -21,7 +21,7 @@ func checkStatus(resp *http.Response) (*http.Response, error) {
21 if resp.StatusCode >= 200 && resp.StatusCode <= 299 {21 if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
22 return resp, nil22 return resp, nil
23 }23 }
24- defer resp.Body.Close()24+ defer func() { _ = resp.Body.Close() }()
25 snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 512))25 snippet, _ := io.ReadAll(io.LimitReader(resp.Body, 512))
26 return nil, &httpStatusError{Status: resp.StatusCode, Body: strings.TrimSpace(string(snippet))}26 return nil, &httpStatusError{Status: resp.StatusCode, Body: strings.TrimSpace(string(snippet))}
27 }27 }
modified internal/mcp/tool_wrapper.go +1 -1
@@ -405,7 +405,7 @@ func callMCPTool(
405405 output.WriteString(content.Text)
406406 } else {
407407 // Non-text content - skip with warning
408- output.WriteString(fmt.Sprintf("[mcp: non-text content of type %s skipped]", content.Type))
408+ fmt.Fprintf(&output, "[mcp: non-text content of type %s skipped]", content.Type)
409409 }
410410 }
411411
@@ -405,7 +405,7 @@ func callMCPTool(
405 output.WriteString(content.Text)405 output.WriteString(content.Text)
406 } else {406 } else {
407 // Non-text content - skip with warning407 // Non-text content - skip with warning
408- output.WriteString(fmt.Sprintf("[mcp: non-text content of type %s skipped]", content.Type))408+ fmt.Fprintf(&output, "[mcp: non-text content of type %s skipped]", content.Type)
409 }409 }
410 }410 }
411 411
modified internal/tools/mcp.go +0 -92
@@ -1,14 +1,9 @@
11 package tools
22
33 import (
4- "context"
5- "fmt"
6-
74 "mm/internal/config"
85 "mm/internal/detector"
96 "mm/internal/mcp"
10- "mm/internal/spinner"
11- "mm/internal/ui"
127
138 "github.com/firebase/genkit/go/ai"
149 "github.com/firebase/genkit/go/genkit"
@@ -35,90 +30,3 @@ func MCP(
3530 // Wrap all tools and get their names
3631 return mcp.WrapToolsWithNames(g, d, clients, defaultPermissions)
3732 }
38-
39-// MCPTool is a pre-configured MCP tool factory for a single server client.
40-// This is useful for testing or when you need to create a tool outside the main MCP setup.
41-func MCPTool(
42- g *genkit.Genkit,
43- d *detector.LoopDetector,
44- client *mcp.ServerClient,
45- permissions string,
46-) (ai.ToolRef, string, error) {
47- if client == nil || len(client.ToolList) == 0 {
48- return nil, "", fmt.Errorf("no tools available from this client")
49- }
50-
51- // For now, just wrap the first tool (simplified)
52- // In a full implementation, this would wrap all tools
53- defaultPermissions := permissions
54- if defaultPermissions == "" {
55- defaultPermissions = config.PermissionsAsk
56- }
57-
58- // This is a simplified version - the full version uses WrapTools
59- // which handles all tools from all clients
60- return nil, "", fmt.Errorf("use MCP() for full tool wrapping")
61-}
62-
63-// MCPEvented is the MCP tool handler for event-consuming front ends.
64-// This is exposed for testing purposes.
65-func MCPEvented(
66- ctx context.Context,
67- s ui.Sink,
68- d *detector.LoopDetector,
69- client *mcp.ServerClient,
70- mcpTool mcp.Tool,
71- toolName string,
72- in map[string]any,
73- permissions string,
74-) (string, error) {
75- // Delegate to the mcp package's internal implementation
76- // This is a pass-through for testing
77- return "", fmt.Errorf("use MCP() for full implementation")
78-}
79-
80-// MCPTerminal is the MCP tool handler for terminal mode.
81-// This is exposed for testing purposes.
82-func MCPTerminal(
83- ctx context.Context,
84- d *detector.LoopDetector,
85- client *mcp.ServerClient,
86- mcpTool mcp.Tool,
87- toolName string,
88- in map[string]any,
89-) (string, error) {
90- // Delegate to the mcp package's internal implementation
91- // This is a pass-through for testing
92- return "", fmt.Errorf("use MCP() for full implementation")
93-}
94-
95-// EchoMCPOutput displays MCP tool output in the terminal.
96-// This is separate from echoOutput to allow for different formatting.
97-func EchoMCPOutput(out string) {
98- if config.Cfg.PreviewLines <= 0 {
99- return
100- }
101-
102- // For now, just use the standard echoOutput
103- // Could be customized for MCP-specific formatting
104- echoOutput(out)
105-}
106-
107-// announceMCP prints the MCP tool call announcement in the terminal.
108-func announceMCP(toolName, description string) {
109- spinner.Suspend(func() {
110- printMu.Lock()
111- fmt.Printf("\n🔧 %s: %s\n", toolName, truncateDesc(description))
112- printMu.Unlock()
113- })
114-}
115-
116-// truncateDesc truncates a description to a reasonable length for display.
117-func truncateDesc(desc string) string {
118- const maxDescLen = 60
119- r := []rune(desc)
120- if len(r) <= maxDescLen {
121- return desc
122- }
123- return string(r[:maxDescLen-3]) + "..."
124-}
@@ -1,14 +1,9 @@
1 package tools1 package tools
2 2
3 import (3 import (
4- "context"
5- "fmt"
6-
7 "mm/internal/config"4 "mm/internal/config"
8 "mm/internal/detector"5 "mm/internal/detector"
9 "mm/internal/mcp"6 "mm/internal/mcp"
10- "mm/internal/spinner"
11- "mm/internal/ui"
12 7
13 "github.com/firebase/genkit/go/ai"8 "github.com/firebase/genkit/go/ai"
14 "github.com/firebase/genkit/go/genkit"9 "github.com/firebase/genkit/go/genkit"
@@ -35,90 +30,3 @@ func MCP(
35 // Wrap all tools and get their names30 // Wrap all tools and get their names
36 return mcp.WrapToolsWithNames(g, d, clients, defaultPermissions)31 return mcp.WrapToolsWithNames(g, d, clients, defaultPermissions)
37 }32 }
38-
39-// MCPTool is a pre-configured MCP tool factory for a single server client.
40-// This is useful for testing or when you need to create a tool outside the main MCP setup.
41-func MCPTool(
42- g *genkit.Genkit,
43- d *detector.LoopDetector,
44- client *mcp.ServerClient,
45- permissions string,
46-) (ai.ToolRef, string, error) {
47- if client == nil || len(client.ToolList) == 0 {
48- return nil, "", fmt.Errorf("no tools available from this client")
49- }
50-
51- // For now, just wrap the first tool (simplified)
52- // In a full implementation, this would wrap all tools
53- defaultPermissions := permissions
54- if defaultPermissions == "" {
55- defaultPermissions = config.PermissionsAsk
56- }
57-
58- // This is a simplified version - the full version uses WrapTools
59- // which handles all tools from all clients
60- return nil, "", fmt.Errorf("use MCP() for full tool wrapping")
61-}
62-
63-// MCPEvented is the MCP tool handler for event-consuming front ends.
64-// This is exposed for testing purposes.
65-func MCPEvented(
66- ctx context.Context,
67- s ui.Sink,
68- d *detector.LoopDetector,
69- client *mcp.ServerClient,
70- mcpTool mcp.Tool,
71- toolName string,
72- in map[string]any,
73- permissions string,
74-) (string, error) {
75- // Delegate to the mcp package's internal implementation
76- // This is a pass-through for testing
77- return "", fmt.Errorf("use MCP() for full implementation")
78-}
79-
80-// MCPTerminal is the MCP tool handler for terminal mode.
81-// This is exposed for testing purposes.
82-func MCPTerminal(
83- ctx context.Context,
84- d *detector.LoopDetector,
85- client *mcp.ServerClient,
86- mcpTool mcp.Tool,
87- toolName string,
88- in map[string]any,
89-) (string, error) {
90- // Delegate to the mcp package's internal implementation
91- // This is a pass-through for testing
92- return "", fmt.Errorf("use MCP() for full implementation")
93-}
94-
95-// EchoMCPOutput displays MCP tool output in the terminal.
96-// This is separate from echoOutput to allow for different formatting.
97-func EchoMCPOutput(out string) {
98- if config.Cfg.PreviewLines <= 0 {
99- return
100- }
101-
102- // For now, just use the standard echoOutput
103- // Could be customized for MCP-specific formatting
104- echoOutput(out)
105-}
106-
107-// announceMCP prints the MCP tool call announcement in the terminal.
108-func announceMCP(toolName, description string) {
109- spinner.Suspend(func() {
110- printMu.Lock()
111- fmt.Printf("\n🔧 %s: %s\n", toolName, truncateDesc(description))
112- printMu.Unlock()
113- })
114-}
115-
116-// truncateDesc truncates a description to a reasonable length for display.
117-func truncateDesc(desc string) string {
118- const maxDescLen = 60
119- r := []rune(desc)
120- if len(r) <= maxDescLen {
121- return desc
122- }
123- return string(r[:maxDescLen-3]) + "..."
124-}
modified internal/tools/plan.go +16 -30
@@ -351,33 +351,14 @@ func (s *PlanStore) GetStatus(name string) (string, string, error) {
351351 if plan.Current == 0 {
352352 // Not started, show Task 1
353353 task := plan.Tasks[0]
354- statusIcon := "⏳"
355- if task.Status == "done" {
356- statusIcon = "✅"
357- } else if task.Status == "in_progress" {
358- statusIcon = "🔄"
359- } else if task.Status == "failed" {
360- statusIcon = "❌"
361- }
362- return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", 1, len(plan.Tasks), statusIcon, 1, task.Description, task.Status),
354+ return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", 1, len(plan.Tasks), statusIcon(task.Status), 1, task.Description, task.Status),
363355 fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil
364356 }
365357
366358 // Current > 0, show Task Current
367359 if plan.Current <= len(plan.Tasks) {
368360 task := plan.Tasks[plan.Current-1]
369- statusIcon := ""
370- switch task.Status {
371- case "done":
372- statusIcon = "✅"
373- case "in_progress":
374- statusIcon = "🔄"
375- case "failed":
376- statusIcon = "❌"
377- default:
378- statusIcon = "⏳"
379- }
380- return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", plan.Current, len(plan.Tasks), statusIcon, plan.Current, task.Description, task.Status),
361+ return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", plan.Current, len(plan.Tasks), statusIcon(task.Status), plan.Current, task.Description, task.Status),
381362 fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil
382363 }
383364
@@ -386,6 +367,20 @@ func (s *PlanStore) GetStatus(name string) (string, string, error) {
386367 fmt.Sprintf("Plan '%s': All %d tasks done ✅", name, len(plan.Tasks)), nil
387368 }
388369
370+// statusIcon is the display mark for a task status.
371+func statusIcon(status string) string {
372+ switch status {
373+ case "done":
374+ return "✅"
375+ case "in_progress":
376+ return "🔄"
377+ case "failed":
378+ return "❌"
379+ default:
380+ return "⏳"
381+ }
382+}
383+
389384 // ClearPlans removes all plans for the current session.
390385 // Called when /new command is issued.
391386 func (s *PlanStore) ClearPlans() {
@@ -402,15 +397,6 @@ func (s *PlanStore) ClearAllPlans() {
402397 s.plans = make(map[string]map[string]*Plan)
403398 }
404399
405-// announcePlan prints the display line for a plan operation.
406-func announcePlan(line string) {
407- spinner.Suspend(func() {
408- printMu.Lock()
409- fmt.Printf("\n📋 %s\n", clip(line))
410- printMu.Unlock()
411- })
412-}
413-
414400 // PlanTool declares the `plan` tool.
415401 func PlanTool(g *genkit.Genkit, d *detector.LoopDetector) ai.ToolRef {
416402 description := `Create and manage multi-step plans. When the user gives you a list of tasks to do (e.g., "Do: 1. X, 2. Y, 3. Z"), use this tool to create a plan and track progress.
@@ -351,33 +351,14 @@ func (s *PlanStore) GetStatus(name string) (string, string, error) {
351 if plan.Current == 0 {351 if plan.Current == 0 {
352 // Not started, show Task 1352 // Not started, show Task 1
353 task := plan.Tasks[0]353 task := plan.Tasks[0]
354- statusIcon := "⏳"354+ return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", 1, len(plan.Tasks), statusIcon(task.Status), 1, task.Description, task.Status),
355- if task.Status == "done" {
356- statusIcon = "✅"
357- } else if task.Status == "in_progress" {
358- statusIcon = "🔄"
359- } else if task.Status == "failed" {
360- statusIcon = "❌"
361- }
362- return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", 1, len(plan.Tasks), statusIcon, 1, task.Description, task.Status),
363 fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil355 fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil
364 }356 }
365 357
366 // Current > 0, show Task Current358 // Current > 0, show Task Current
367 if plan.Current <= len(plan.Tasks) {359 if plan.Current <= len(plan.Tasks) {
368 task := plan.Tasks[plan.Current-1]360 task := plan.Tasks[plan.Current-1]
369- statusIcon := ""361+ return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", plan.Current, len(plan.Tasks), statusIcon(task.Status), plan.Current, task.Description, task.Status),
370- switch task.Status {
371- case "done":
372- statusIcon = "✅"
373- case "in_progress":
374- statusIcon = "🔄"
375- case "failed":
376- statusIcon = "❌"
377- default:
378- statusIcon = "⏳"
379- }
380- return fmt.Sprintf("[%d/%d] %s Task %d: %s (%s)", plan.Current, len(plan.Tasks), statusIcon, plan.Current, task.Description, task.Status),
381 fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil362 fmt.Sprintf("Plan '%s': %d/%d tasks complete", name, doneCount, len(plan.Tasks)), nil
382 }363 }
383 364
@@ -386,6 +367,20 @@ func (s *PlanStore) GetStatus(name string) (string, string, error) {
386 fmt.Sprintf("Plan '%s': All %d tasks done ✅", name, len(plan.Tasks)), nil367 fmt.Sprintf("Plan '%s': All %d tasks done ✅", name, len(plan.Tasks)), nil
387 }368 }
388 369
370+// statusIcon is the display mark for a task status.
371+func statusIcon(status string) string {
372+ switch status {
373+ case "done":
374+ return "✅"
375+ case "in_progress":
376+ return "🔄"
377+ case "failed":
378+ return "❌"
379+ default:
380+ return "⏳"
381+ }
382+}
383+
389 // ClearPlans removes all plans for the current session.384 // ClearPlans removes all plans for the current session.
390 // Called when /new command is issued.385 // Called when /new command is issued.
391 func (s *PlanStore) ClearPlans() {386 func (s *PlanStore) ClearPlans() {
@@ -402,15 +397,6 @@ func (s *PlanStore) ClearAllPlans() {
402 s.plans = make(map[string]map[string]*Plan)397 s.plans = make(map[string]map[string]*Plan)
403 }398 }
404 399
405-// announcePlan prints the display line for a plan operation.
406-func announcePlan(line string) {
407- spinner.Suspend(func() {
408- printMu.Lock()
409- fmt.Printf("\n📋 %s\n", clip(line))
410- printMu.Unlock()
411- })
412-}
413-
414 // PlanTool declares the `plan` tool.400 // PlanTool declares the `plan` tool.
415 func PlanTool(g *genkit.Genkit, d *detector.LoopDetector) ai.ToolRef {401 func PlanTool(g *genkit.Genkit, d *detector.LoopDetector) ai.ToolRef {
416 description := `Create and manage multi-step plans. When the user gives you a list of tasks to do (e.g., "Do: 1. X, 2. Y, 3. Z"), use this tool to create a plan and track progress.402 description := `Create and manage multi-step plans. When the user gives you a list of tasks to do (e.g., "Do: 1. X, 2. Y, 3. Z"), use this tool to create a plan and track progress.
modified internal/tools/plan_test.go +21 -12
@@ -227,7 +227,7 @@ func TestPlanStore_NextTask(t *testing.T) {
227227 }
228228
229229 // Second next - should mark task 1 as done, go to task 2
230- plan, nextTask, msg, err = store.NextTask("next-test")
230+ plan, nextTask, _, err = store.NextTask("next-test")
231231 if err != nil {
232232 t.Fatalf("Second NextTask failed: %v", err)
233233 }
@@ -242,7 +242,7 @@ func TestPlanStore_NextTask(t *testing.T) {
242242 }
243243
244244 // Third next - should mark task 2 as done, go to task 3
245- plan, nextTask, msg, err = store.NextTask("next-test")
245+ plan, nextTask, _, err = store.NextTask("next-test")
246246 if err != nil {
247247 t.Fatalf("Third NextTask failed: %v", err)
248248 }
@@ -362,9 +362,12 @@ func TestPlanStore_GetStatus(t *testing.T) {
362362 }
363363
364364 // Complete all tasks (need 3 more NextTask calls to complete all 3 tasks)
365- store.NextTask("status-test") // Marks Task 1 as done, moves to Task 2
366- store.NextTask("status-test") // Marks Task 2 as done, moves to Task 3
367- store.NextTask("status-test") // Marks Task 3 as done, moves to Current=4 (complete)
365+ // Each call marks the current task done and moves on; the third reaches Current=4 (complete).
366+ for i := 0; i < 3; i++ {
367+ if _, _, _, err := store.NextTask("status-test"); err != nil {
368+ t.Fatalf("NextTask %d failed: %v", i+1, err)
369+ }
370+ }
368371
369372 // Get status when complete
370373 statusLine, _, err = store.GetStatus("status-test")
@@ -396,9 +399,7 @@ func TestPlanStore_ListPlans(t *testing.T) {
396399 }
397400
398401 // Create multiple plans
399- store.CreatePlan("plan-a", []PlanTaskInput{{Description: "A1"}})
400- store.CreatePlan("plan-b", []PlanTaskInput{{Description: "B1"}})
401- store.CreatePlan("plan-c", []PlanTaskInput{{Description: "C1"}})
402+ mustCreatePlans(t, store, "plan-a", "plan-b", "plan-c")
402403
403404 // List plans
404405 plans, msg, err = store.ListPlans()
@@ -419,8 +420,7 @@ func TestPlanStore_ClearPlans(t *testing.T) {
419420 store := planStore
420421
421422 // Create some plans
422- store.CreatePlan("clear-test-1", []PlanTaskInput{{Description: "T1"}})
423- store.CreatePlan("clear-test-2", []PlanTaskInput{{Description: "T2"}})
423+ mustCreatePlans(t, store, "clear-test-1", "clear-test-2")
424424
425425 // Verify plans exist
426426 plans, _, _ := store.ListPlans()
@@ -444,8 +444,7 @@ func TestPlanStore_ClearAllPlans(t *testing.T) {
444444 store := planStore
445445
446446 // Create some plans
447- store.CreatePlan("all-clear-1", []PlanTaskInput{{Description: "T1"}})
448- store.CreatePlan("all-clear-2", []PlanTaskInput{{Description: "T2"}})
447+ mustCreatePlans(t, store, "all-clear-1", "all-clear-2")
449448
450449 // Clear all
451450 store.ClearAllPlans()
@@ -540,3 +539,13 @@ func TestPlanTool_CreateAndStatus(t *testing.T) {
540539 // The store tests above verify the core functionality
541540 t.Skip("Skipping tool integration test - requires full Genkit setup")
542541 }
542+
543+// mustCreatePlans creates one single-task plan per name, failing the test on error.
544+func mustCreatePlans(t *testing.T, store *PlanStore, names ...string) {
545+ t.Helper()
546+ for _, name := range names {
547+ if _, _, err := store.CreatePlan(name, []PlanTaskInput{{Description: "task of " + name}}); err != nil {
548+ t.Fatalf("CreatePlan %s failed: %v", name, err)
549+ }
550+ }
551+}
@@ -227,7 +227,7 @@ func TestPlanStore_NextTask(t *testing.T) {
227 }227 }
228 228
229 // Second next - should mark task 1 as done, go to task 2229 // Second next - should mark task 1 as done, go to task 2
230- plan, nextTask, msg, err = store.NextTask("next-test")230+ plan, nextTask, _, err = store.NextTask("next-test")
231 if err != nil {231 if err != nil {
232 t.Fatalf("Second NextTask failed: %v", err)232 t.Fatalf("Second NextTask failed: %v", err)
233 }233 }
@@ -242,7 +242,7 @@ func TestPlanStore_NextTask(t *testing.T) {
242 }242 }
243 243
244 // Third next - should mark task 2 as done, go to task 3244 // Third next - should mark task 2 as done, go to task 3
245- plan, nextTask, msg, err = store.NextTask("next-test")245+ plan, nextTask, _, err = store.NextTask("next-test")
246 if err != nil {246 if err != nil {
247 t.Fatalf("Third NextTask failed: %v", err)247 t.Fatalf("Third NextTask failed: %v", err)
248 }248 }
@@ -362,9 +362,12 @@ func TestPlanStore_GetStatus(t *testing.T) {
362 }362 }
363 363
364 // Complete all tasks (need 3 more NextTask calls to complete all 3 tasks)364 // Complete all tasks (need 3 more NextTask calls to complete all 3 tasks)
365- store.NextTask("status-test") // Marks Task 1 as done, moves to Task 2365+ // Each call marks the current task done and moves on; the third reaches Current=4 (complete).
366- store.NextTask("status-test") // Marks Task 2 as done, moves to Task 3366+ for i := 0; i < 3; i++ {
367- store.NextTask("status-test") // Marks Task 3 as done, moves to Current=4 (complete)367+ if _, _, _, err := store.NextTask("status-test"); err != nil {
368+ t.Fatalf("NextTask %d failed: %v", i+1, err)
369+ }
370+ }
368 371
369 // Get status when complete372 // Get status when complete
370 statusLine, _, err = store.GetStatus("status-test")373 statusLine, _, err = store.GetStatus("status-test")
@@ -396,9 +399,7 @@ func TestPlanStore_ListPlans(t *testing.T) {
396 }399 }
397 400
398 // Create multiple plans401 // Create multiple plans
399- store.CreatePlan("plan-a", []PlanTaskInput{{Description: "A1"}})402+ mustCreatePlans(t, store, "plan-a", "plan-b", "plan-c")
400- store.CreatePlan("plan-b", []PlanTaskInput{{Description: "B1"}})
401- store.CreatePlan("plan-c", []PlanTaskInput{{Description: "C1"}})
402 403
403 // List plans404 // List plans
404 plans, msg, err = store.ListPlans()405 plans, msg, err = store.ListPlans()
@@ -419,8 +420,7 @@ func TestPlanStore_ClearPlans(t *testing.T) {
419 store := planStore420 store := planStore
420 421
421 // Create some plans422 // Create some plans
422- store.CreatePlan("clear-test-1", []PlanTaskInput{{Description: "T1"}})423+ mustCreatePlans(t, store, "clear-test-1", "clear-test-2")
423- store.CreatePlan("clear-test-2", []PlanTaskInput{{Description: "T2"}})
424 424
425 // Verify plans exist425 // Verify plans exist
426 plans, _, _ := store.ListPlans()426 plans, _, _ := store.ListPlans()
@@ -444,8 +444,7 @@ func TestPlanStore_ClearAllPlans(t *testing.T) {
444 store := planStore444 store := planStore
445 445
446 // Create some plans446 // Create some plans
447- store.CreatePlan("all-clear-1", []PlanTaskInput{{Description: "T1"}})447+ mustCreatePlans(t, store, "all-clear-1", "all-clear-2")
448- store.CreatePlan("all-clear-2", []PlanTaskInput{{Description: "T2"}})
449 448
450 // Clear all449 // Clear all
451 store.ClearAllPlans()450 store.ClearAllPlans()
@@ -540,3 +539,13 @@ func TestPlanTool_CreateAndStatus(t *testing.T) {
540 // The store tests above verify the core functionality539 // The store tests above verify the core functionality
541 t.Skip("Skipping tool integration test - requires full Genkit setup")540 t.Skip("Skipping tool integration test - requires full Genkit setup")
542 }541 }
542+
543+// mustCreatePlans creates one single-task plan per name, failing the test on error.
544+func mustCreatePlans(t *testing.T, store *PlanStore, names ...string) {
545+ t.Helper()
546+ for _, name := range names {
547+ if _, _, err := store.CreatePlan(name, []PlanTaskInput{{Description: "task of " + name}}); err != nil {
548+ t.Fatalf("CreatePlan %s failed: %v", name, err)
549+ }
550+ }
551+}
modified internal/tools/tools.go +1 -1
@@ -153,7 +153,7 @@ func runBash(ctx context.Context, dir, command string) (output string, failed bo
153153 // Unlinked at once: the descriptor stays valid for us and for whatever the
154154 // command leaves running, and nothing is left in /tmp if mm is killed.
155155 _ = os.Remove(sink.Name())
156- defer sink.Close()
156+ defer func() { _ = sink.Close() }()
157157
158158 cmd := exec.CommandContext(ctx, "bash", "-c", command)
159159 cmd.Dir = dir
@@ -153,7 +153,7 @@ func runBash(ctx context.Context, dir, command string) (output string, failed bo
153 // Unlinked at once: the descriptor stays valid for us and for whatever the153 // Unlinked at once: the descriptor stays valid for us and for whatever the
154 // command leaves running, and nothing is left in /tmp if mm is killed.154 // command leaves running, and nothing is left in /tmp if mm is killed.
155 _ = os.Remove(sink.Name())155 _ = os.Remove(sink.Name())
156- defer sink.Close()156+ defer func() { _ = sink.Close() }()
157 157
158 cmd := exec.CommandContext(ctx, "bash", "-c", command)158 cmd := exec.CommandContext(ctx, "bash", "-c", command)
159 cmd.Dir = dir159 cmd.Dir = dir
modified internal/tui/question.go +3 -2
@@ -286,13 +286,14 @@ func (v *View) drawFormChoices(p painter) {
286286 if at == f.cursor {
287287 mark = "❯ "
288288 }
289- if q.Kind == elicit.Multi {
289+ switch q.Kind {
290+ case elicit.Multi:
290291 box := "[ ] "
291292 if f.checked[at] {
292293 box = "[x] "
293294 }
294295 mark += box
295- } else if q.Kind == elicit.Choice {
296+ case elicit.Choice:
296297 mark += fmt.Sprintf("%d. ", at+1)
297298 }
298299 style := stBody
@@ -286,13 +286,14 @@ func (v *View) drawFormChoices(p painter) {
286 if at == f.cursor {286 if at == f.cursor {
287 mark = "❯ "287 mark = "❯ "
288 }288 }
289- if q.Kind == elicit.Multi {289+ switch q.Kind {
290+ case elicit.Multi:
290 box := "[ ] "291 box := "[ ] "
291 if f.checked[at] {292 if f.checked[at] {
292 box = "[x] "293 box = "[x] "
293 }294 }
294 mark += box295 mark += box
295- } else if q.Kind == elicit.Choice {296+ case elicit.Choice:
296 mark += fmt.Sprintf("%d. ", at+1)297 mark += fmt.Sprintf("%d. ", at+1)
297 }298 }
298 style := stBody299 style := stBody
modified internal/ui/ui.go +1 -1
@@ -132,7 +132,7 @@ func Notice(text string) {
132132 n.Notice(text)
133133 return
134134 }
135- fmt.Fprintln(Out, text)
135+ _, _ = fmt.Fprintln(Out, text)
136136 }
137137
138138 // Speaker is the optional side of a Sink that shows WHICH agent is answering:
@@ -132,7 +132,7 @@ func Notice(text string) {
132 n.Notice(text)132 n.Notice(text)
133 return133 return
134 }134 }
135- fmt.Fprintln(Out, text)135+ _, _ = fmt.Fprintln(Out, text)
136 }136 }
137 137
138 // Speaker is the optional side of a Sink that shows WHICH agent is answering:138 // Speaker is the optional side of a Sink that shows WHICH agent is answering: