From b90f49970e1eff4c2b0179b466853b34cb603dc5 Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Sat, 28 Feb 2026 23:53:23 +0900 Subject: [PATCH] style: fix golines, gofmt, and gosmopolitan lint errors - Run golines (max 120 chars) on long lines - Run gofmt with interface{} -> any rewrite rule - Add nolint:gosmopolitan for intentional CJK strings Co-Authored-By: Claude Opus 4.6 --- cmd/picoclaw/internal/gateway/helpers.go | 21 ++++- pkg/agent/loop.go | 82 ++++++++++++----- pkg/agent/loop_test.go | 102 +++++++++++++--------- pkg/agent/memory.go | 20 +++-- pkg/config/config_test.go | 3 +- pkg/git/worktree_test.go | 5 +- pkg/logger/logger_test.go | 30 +++++-- pkg/miniapp/api.go | 8 +- pkg/miniapp/miniapp.go | 9 +- pkg/providers/claude_cli_provider_test.go | 2 +- pkg/providers/http_provider.go | 8 +- pkg/providers/openai_compat/provider.go | 12 ++- pkg/providers/tool_call_extract.go | 5 +- pkg/tools/dev_preview.go | 9 +- pkg/tools/shell.go | 4 +- pkg/tools/subagent_reporter_test.go | 8 +- 16 files changed, 235 insertions(+), 93 deletions(-) diff --git a/cmd/picoclaw/internal/gateway/helpers.go b/cmd/picoclaw/internal/gateway/helpers.go index e62bff456..abedbbbd7 100644 --- a/cmd/picoclaw/internal/gateway/helpers.go +++ b/cmd/picoclaw/internal/gateway/helpers.go @@ -205,7 +205,11 @@ func gatewayCmd(debug bool, orchestration bool) error { // Auto-detect Tailscale hostname and fetch TLS cert hostname, tsErr := tailscale.DetectHostname() if tsErr != nil { - logger.InfoCF("miniapp", "Tailscale not available, Mini App disabled", map[string]any{"error": tsErr.Error()}) + logger.InfoCF( + "miniapp", + "Tailscale not available, Mini App disabled", + map[string]any{"error": tsErr.Error()}, + ) } else { certDir := filepath.Join(cfg.WorkspacePath(), "state", "certs") certFile, keyFile, certErr := tailscale.FetchCert(hostname, certDir) @@ -224,7 +228,14 @@ func gatewayCmd(debug bool, orchestration bool) error { dataProvider := &agentLoopDataProvider{loop: agentLoop, workspace: cfg.WorkspacePath()} sender := &telegramCommandSender{bus: msgBus} miniappNotifier = miniapp.NewStateNotifier() - handler := miniapp.NewHandler(dataProvider, sender, cfg.Channels.Telegram.Token, miniappNotifier, cfg.Channels.Telegram.AllowFrom, cfg.WorkspacePath()) + handler := miniapp.NewHandler( + dataProvider, + sender, + cfg.Channels.Telegram.Token, + miniappNotifier, + cfg.Channels.Telegram.AllowFrom, + cfg.WorkspacePath(), + ) agentLoop.OnStateChange = miniappNotifier.Notify if b := agentLoop.GetOrchBroadcaster(); b != nil { handler.SetOrchBroadcaster(b) @@ -251,7 +262,11 @@ func gatewayCmd(debug bool, orchestration bool) error { } }() if useTLS { - fmt.Printf("✓ Health endpoints available at https://%s:%d/health and /ready (TLS)\n", cfg.Gateway.Host, cfg.Gateway.Port) + fmt.Printf( + "✓ Health endpoints available at https://%s:%d/health and /ready (TLS)\n", + cfg.Gateway.Host, + cfg.Gateway.Port, + ) } else { fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port) } diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 6914ede4e..9c2a80088 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -119,7 +119,12 @@ type processOptions struct { const defaultResponse = "I've completed processing but have no response to give. Increase `max_tool_iterations` in config.json." -func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers.LLMProvider, enableStats ...bool) *AgentLoop { +func NewAgentLoop( + cfg *config.Config, + msgBus *bus.MessageBus, + provider providers.LLMProvider, + enableStats ...bool, +) *AgentLoop { registry := NewAgentRegistry(cfg, provider) // Set up shared fallback chain @@ -282,7 +287,14 @@ func registerSharedTools( PerplexityMaxResults: cfg.Tools.Web.Perplexity.MaxResults, PerplexityEnabled: cfg.Tools.Web.Perplexity.Enabled, } - subagentManager := tools.NewSubagentManager(provider, agent.Model, agent.Workspace, msgBus, al.reporter(), webSearchOpts) + subagentManager := tools.NewSubagentManager( + provider, + agent.Model, + agent.Workspace, + msgBus, + al.reporter(), + webSearchOpts, + ) subagentManager.SetLLMOptions(agent.MaxTokens, agent.Temperature) spawnTool := tools.NewSpawnTool(subagentManager) currentAgentID := agentID @@ -480,7 +492,10 @@ func (al *AgentLoop) SetChannelManager(cm *channels.Manager) { // It caches created providers by "provider/model" key so each combination is // only resolved once. Looks up model_list first (new format), then falls back // to the legacy providers section via CreateProviderByName. -func (al *AgentLoop) resolveProvider(providerName, modelName string, fallback providers.LLMProvider) providers.LLMProvider { +func (al *AgentLoop) resolveProvider( + providerName, modelName string, + fallback providers.LLMProvider, +) providers.LLMProvider { key := strings.ToLower(providerName + "/" + modelName) if key == "/" { return fallback @@ -497,14 +512,14 @@ func (al *AgentLoop) resolveProvider(providerName, modelName string, fallback pr return p } logger.WarnCF("agent", "Failed to create provider from model_list, trying legacy", - map[string]interface{}{"provider": providerName, "model": modelName, "error": err.Error()}) + map[string]any{"provider": providerName, "model": modelName, "error": err.Error()}) } // Fall back to legacy providers section. p, err := providers.CreateProviderByName(al.cfg, providerName) if err != nil { logger.WarnCF("agent", "Failed to create provider for fallback, using primary", - map[string]interface{}{"provider": providerName, "error": err.Error()}) + map[string]any{"provider": providerName, "error": err.Error()}) return fallback } al.providerCache[key] = p @@ -630,7 +645,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage) lower := strings.ToLower(content) // Check for stop keywords - stopKeywords := []string{"stop", "cancel", "abort", "停止", "中止", "やめて"} + stopKeywords := []string{"stop", "cancel", "abort", "停止", "中止", "やめて"} //nolint:gosmopolitan // intentional CJK stop words isStop := false for _, kw := range stopKeywords { if lower == kw { @@ -939,7 +954,8 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt // 1a. Set session-specific working directory for bootstrap file lookup. // Prefer the tool-detected project directory (touch_dir) from the session tracker, // resolved as an absolute path under workspace. Fall back to worktree or workspace. - if active := al.sessions.ListActive(); len(active) > 0 && active[0].SessionKey == opts.SessionKey && active[0].TouchDir != "" { + if active := al.sessions.ListActive(); len(active) > 0 && active[0].SessionKey == opts.SessionKey && + active[0].TouchDir != "" { agent.ContextBuilder.SetWorkDir(filepath.Join(agent.Workspace, active[0].TouchDir)) } else { agent.ContextBuilder.SetWorkDir(agent.EffectiveWorkspace(opts.SessionKey)) @@ -1015,7 +1031,9 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt sb.WriteString("\n\n## Background Execution\n") sb.WriteString("You are running as a background heartbeat with no conversation history. ") sb.WriteString("MEMORY.md is the only shared state between heartbeats. ") - sb.WriteString("After completing each plan step, immediately use edit_file to mark it [x] in memory/MEMORY.md.") + sb.WriteString( + "After completing each plan step, immediately use edit_file to mark it [x] in memory/MEMORY.md.", + ) messages[0].Content = sb.String() } } @@ -1053,14 +1071,15 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt // 5a. Auto-advance plan phases after LLM iteration postStatus := agent.ContextBuilder.GetPlanStatus() - if agent.ContextBuilder.HasActivePlan() && (postStatus == "executing" || postStatus == "review" || postStatus == "completed") { + if agent.ContextBuilder.HasActivePlan() && + (postStatus == "executing" || postStatus == "review" || postStatus == "completed") { // Intercept: if AI changed status to executing or review without user approval // (from interviewing or review), validate and hold at "review". if preStatus == "interviewing" || (preStatus == "review" && postStatus == "executing") { if err := agent.ContextBuilder.ValidatePlanStructure(); err != nil { _ = agent.ContextBuilder.SetPlanStatus("interviewing") logger.WarnCF("agent", "Reverted plan to interviewing: "+err.Error(), - map[string]interface{}{"agent_id": agent.ID}) + map[string]any{"agent_id": agent.ID}) // Inject rejection into session history so LLM sees it next iteration rejectionMsg := "[System] Plan rejected: " + err.Error() + ". Fix and try again." agent.Sessions.AddMessage(opts.SessionKey, "user", rejectionMsg) @@ -1080,7 +1099,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt // Safeguard: executing but no phases (shouldn't happen, but be safe). _ = agent.ContextBuilder.SetPlanStatus("interviewing") logger.WarnCF("agent", "Reverted plan to interviewing: no phases defined", - map[string]interface{}{"agent_id": agent.ID}) + map[string]any{"agent_id": agent.ID}) } else if agent.ContextBuilder.IsPlanComplete() { // Mark plan as completed (keep memory for review; user can /plan clear) total := agent.ContextBuilder.GetTotalPhases() @@ -1194,7 +1213,8 @@ func buildTaskReminder(userMessage string, lastBlocker string) providers.Message truncatedBlocker := utils.Truncate(lastBlocker, blockerMaxChars) content = fmt.Sprintf( "[TASK REMINDER]\nOriginal task:\n---\n%s\n---\nLast blocker:\n---\n%s\n---\nFix the blocker if essential, or find an alternative. If all steps are complete, move on.", - truncatedTask, truncatedBlocker, + truncatedTask, + truncatedBlocker, ) } else { content = fmt.Sprintf( @@ -1247,7 +1267,7 @@ var optFlagPattern = regexp.MustCompile(`\s+--?\w[\w-]*(=\S*)?`) // extractExecProjectDir extracts the basename of an exec cd target. // Returns "" if the command has no cd prefix. -func extractExecProjectDir(args map[string]interface{}) string { +func extractExecProjectDir(args map[string]any) string { cmd, _ := args["command"].(string) if cmd == "" { return "" @@ -1338,7 +1358,7 @@ func (al *AgentLoop) targetReasoningChannelID(channelName string) (chatID string // For exec: extracts the command and strips the leading "cd && ". // For file tools: extracts the path and strips the workspace prefix. // Falls back to raw JSON truncation. -func buildArgsSnippet(toolName string, args map[string]interface{}, workspace string) string { +func buildArgsSnippet(toolName string, args map[string]any, workspace string) string { switch toolName { case "exec": cmd, _ := args["command"].(string) @@ -1829,7 +1849,12 @@ type streamToolCallAcc struct { } // buildAccumulatedResponse constructs an LLMResponse from accumulated stream data. -func buildAccumulatedResponse(content, reasoning string, toolCalls []streamToolCallAcc, finishReason string, usage *providers.UsageInfo) *providers.LLMResponse { +func buildAccumulatedResponse( + content, reasoning string, + toolCalls []streamToolCallAcc, + finishReason string, + usage *providers.UsageInfo, +) *providers.LLMResponse { resp := &providers.LLMResponse{ Content: content, Reasoning: reasoning, @@ -2426,7 +2451,14 @@ func (al *AgentLoop) runLLMIteration( if wt := agent.GetWorktree(opts.SessionKey); wt != nil { toolCtx = tools.WithWorkspaceOverride(toolCtx, wt.Path) } - toolResult := agent.Tools.ExecuteWithContext(toolCtx, tc.Name, tc.Arguments, opts.Channel, opts.ChatID, asyncCallback) + toolResult := agent.Tools.ExecuteWithContext( + toolCtx, + tc.Name, + tc.Arguments, + opts.Channel, + opts.ChatID, + asyncCallback, + ) toolDuration := time.Since(toolStart) // Update tool log entry with result @@ -2539,7 +2571,7 @@ func (al *AgentLoop) runLLMIteration( messages = append(messages, reminderMsg) lastReminderIdx = len(messages) - 1 logger.DebugCF("agent", "Injected task reminder", - map[string]interface{}{ + map[string]any{ "agent_id": agent.ID, "iteration": iteration, "has_blocker": lastBlocker != "", @@ -2551,7 +2583,7 @@ func (al *AgentLoop) runLLMIteration( if reminder, ok := buildPlanReminder(planSnapshot); ok { messages = append(messages, reminder) logger.DebugCF("agent", "Injected plan reminder", - map[string]interface{}{ + map[string]any{ "agent_id": agent.ID, "iteration": iteration, "plan_status": planSnapshot, @@ -2565,7 +2597,8 @@ func (al *AgentLoop) runLLMIteration( if touchDir := al.sessions.GetTouchDir(opts.SessionKey); touchDir != "" { agent.ContextBuilder.SetWorkDir(filepath.Join(agent.Workspace, touchDir)) } - if newPrompt := agent.ContextBuilder.BuildSystemPrompt(); len(messages) > 0 && messages[0].Content != newPrompt { + if newPrompt := agent.ContextBuilder.BuildSystemPrompt(); len(messages) > 0 && + messages[0].Content != newPrompt { messages[0].Content = newPrompt al.lastSystemPrompt.Store(newPrompt) al.promptDirty.Store(false) @@ -2577,11 +2610,11 @@ func (al *AgentLoop) runLLMIteration( // make one final LLM call without tools to force a text response. if finalContent == "" && iteration >= maxIter { logger.WarnCF("agent", "Max iterations reached, forcing final response without tools", - map[string]interface{}{ + map[string]any{ "agent_id": agent.ID, "iteration": iteration, }) - forceResp, forceErr := agent.Provider.Chat(ctx, messages, nil, agent.Model, map[string]interface{}{ + forceResp, forceErr := agent.Provider.Chat(ctx, messages, nil, agent.Model, map[string]any{ "max_tokens": agent.MaxTokens, "temperature": agent.Temperature, "prompt_cache_key": agent.ID, @@ -2633,7 +2666,7 @@ func (al *AgentLoop) maybeSummarize(agent *AgentInstance, sessionKey, channel, c go func() { defer al.summarizing.Delete(summarizeKey) logger.InfoCF("agent", "Memory threshold reached, optimizing conversation history", - map[string]interface{}{ + map[string]any{ "session_key": sessionKey, "history_len": len(newHistory), "token_estimate": tokenEstimate, @@ -3127,7 +3160,8 @@ func (al *AgentLoop) handleSessionCommand(args []string) string { } s := al.stats.GetStats() - return fmt.Sprintf("Session Statistics\n\nToday (%s):\n Prompts: %d\n LLM calls: %d\n Tokens: %s (in: %s, out: %s)\n\nAll time (since %s):\n Prompts: %d\n LLM calls: %d\n Tokens: %s (in: %s, out: %s)", + return fmt.Sprintf( + "Session Statistics\n\nToday (%s):\n Prompts: %d\n LLM calls: %d\n Tokens: %s (in: %s, out: %s)\n\nAll time (since %s):\n Prompts: %d\n LLM calls: %d\n Tokens: %s (in: %s, out: %s)", s.Today.Date, s.Today.Prompts, s.Today.Requests, @@ -3369,7 +3403,7 @@ func filterInterviewTools(defs []providers.ToolDefinition) []providers.ToolDefin // plan is in a pre-execution state. Uses the shared interviewAllowedTools map for // name-level gating, then applies argument-level constraints for write-type tools // (MEMORY.md only) and exec (read-only commands only). -func isToolAllowedDuringInterview(toolName string, args map[string]interface{}) bool { +func isToolAllowedDuringInterview(toolName string, args map[string]any) bool { norm := tools.NormalizeToolName(toolName) if !interviewAllowedTools[norm] { return false diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index d8d998f2f..96f3e7af5 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -1466,7 +1466,7 @@ Test func TestIsToolAllowedDuringInterview_FuzzyNames(t *testing.T) { tests := []struct { name string - args map[string]interface{} + args map[string]any want bool }{ // Exact names — read tools allowed @@ -1484,38 +1484,38 @@ func TestIsToolAllowedDuringInterview_FuzzyNames(t *testing.T) { {"message", nil, true}, {"Message", nil, true}, // Write to MEMORY.md — allowed - {"edit_file", map[string]interface{}{"path": "/ws/memory/MEMORY.md"}, true}, - {"editfile", map[string]interface{}{"path": "/ws/memory/MEMORY.md"}, true}, - {"EditFile", map[string]interface{}{"path": "/ws/memory/MEMORY.md"}, true}, + {"edit_file", map[string]any{"path": "/ws/memory/MEMORY.md"}, true}, + {"editfile", map[string]any{"path": "/ws/memory/MEMORY.md"}, true}, + {"EditFile", map[string]any{"path": "/ws/memory/MEMORY.md"}, true}, // Write to non-MEMORY.md — blocked - {"edit_file", map[string]interface{}{"path": "/ws/main.go"}, false}, - {"editfile", map[string]interface{}{"path": "/ws/main.go"}, false}, + {"edit_file", map[string]any{"path": "/ws/main.go"}, false}, + {"editfile", map[string]any{"path": "/ws/main.go"}, false}, // exec — read-only commands allowed - {"exec", map[string]interface{}{"command": "find . -name '*.py'"}, true}, - {"exec", map[string]interface{}{"command": "ls -la"}, true}, - {"exec", map[string]interface{}{"command": "grep -r TODO ."}, true}, - {"exec", map[string]interface{}{"command": "cat README.md"}, true}, + {"exec", map[string]any{"command": "find . -name '*.py'"}, true}, + {"exec", map[string]any{"command": "ls -la"}, true}, + {"exec", map[string]any{"command": "grep -r TODO ."}, true}, + {"exec", map[string]any{"command": "cat README.md"}, true}, // exec — cd prefix stripped - {"exec", map[string]interface{}{"command": "cd /home/user/project && find . -type f"}, true}, - {"exec", map[string]interface{}{"command": "cd /tmp && rm -rf *"}, false}, + {"exec", map[string]any{"command": "cd /home/user/project && find . -type f"}, true}, + {"exec", map[string]any{"command": "cd /tmp && rm -rf *"}, false}, // exec — write operators blocked - {"exec", map[string]interface{}{"command": "find . > output.txt"}, false}, - {"exec", map[string]interface{}{"command": "ls -la >> log.txt"}, false}, - {"exec", map[string]interface{}{"command": "cat foo | tee bar.txt"}, false}, + {"exec", map[string]any{"command": "find . > output.txt"}, false}, + {"exec", map[string]any{"command": "ls -la >> log.txt"}, false}, + {"exec", map[string]any{"command": "cat foo | tee bar.txt"}, false}, // exec — path traversal blocked - {"exec", map[string]interface{}{"command": "cat ../../etc/passwd"}, false}, - {"exec", map[string]interface{}{"command": "find ../../"}, false}, - {"exec", map[string]interface{}{"command": "ls ../secret"}, false}, + {"exec", map[string]any{"command": "cat ../../etc/passwd"}, false}, + {"exec", map[string]any{"command": "find ../../"}, false}, + {"exec", map[string]any{"command": "ls ../secret"}, false}, // exec — absolute paths blocked - {"exec", map[string]interface{}{"command": "cat /etc/passwd"}, false}, - {"exec", map[string]interface{}{"command": "find /etc -name '*.conf'"}, false}, - {"exec", map[string]interface{}{"command": "ls /root"}, false}, + {"exec", map[string]any{"command": "cat /etc/passwd"}, false}, + {"exec", map[string]any{"command": "find /etc -name '*.conf'"}, false}, + {"exec", map[string]any{"command": "ls /root"}, false}, // exec — write commands blocked - {"exec", map[string]interface{}{"command": "rm -rf /"}, false}, - {"exec", map[string]interface{}{"command": "mv a b"}, false}, + {"exec", map[string]any{"command": "rm -rf /"}, false}, + {"exec", map[string]any{"command": "mv a b"}, false}, // exec — no args / empty command blocked {"exec", nil, false}, - {"exec", map[string]interface{}{"command": ""}, false}, + {"exec", map[string]any{"command": ""}, false}, {"Exec", nil, false}, } for _, tt := range tests { @@ -1530,56 +1530,60 @@ func TestBuildArgsSnippet_ExecStripsCD(t *testing.T) { tests := []struct { name string tool string - args map[string]interface{} + args map[string]any workspace string wantSnip string }{ { - name: "exec strips cd prefix", - tool: "exec", - args: map[string]interface{}{"command": "cd /home/user/workspace/project/my-projects && pytest tests/test_integration.py"}, + name: "exec strips cd prefix", + tool: "exec", + args: map[string]any{ + "command": "cd /home/user/workspace/project/my-projects && pytest tests/test_integration.py", + }, workspace: "/home/user/workspace", wantSnip: "pytest tests/test_integration.py", }, { name: "exec no cd prefix, flags stripped", tool: "exec", - args: map[string]interface{}{"command": "ls -la"}, + args: map[string]any{"command": "ls -la"}, workspace: "/ws", wantSnip: "ls", }, { name: "exec empty command", tool: "exec", - args: map[string]interface{}{}, + args: map[string]any{}, workspace: "/ws", wantSnip: "{}", }, { name: "read_file strips workspace", tool: "read_file", - args: map[string]interface{}{"path": "/home/user/workspace/src/main.go"}, + args: map[string]any{"path": "/home/user/workspace/src/main.go"}, workspace: "/home/user/workspace", wantSnip: "src/main.go", }, { name: "edit_file shows path", tool: "edit_file", - args: map[string]interface{}{"path": "/ws/config.json", "old_text": "old value here"}, + args: map[string]any{"path": "/ws/config.json", "old_text": "old value here"}, workspace: "/ws", wantSnip: "config.json", }, { - name: "file tool long path prioritizes filename", - tool: "read_file", - args: map[string]interface{}{"path": "/ws/projects/terra-py-form/src/terra_py_form/hot/state/backend.py"}, + name: "file tool long path prioritizes filename", + tool: "read_file", + args: map[string]any{ + "path": "/ws/projects/terra-py-form/src/terra_py_form/hot/state/backend.py", + }, workspace: "/ws", wantSnip: "projects/terra-py-form/src/terra_py_form/hot/sta\u2026/backend.py", }, { name: "unknown tool shows raw JSON", tool: "web_search", - args: map[string]interface{}{"query": "hello"}, + args: map[string]any{"query": "hello"}, workspace: "/ws", wantSnip: `{"query":"hello"}`, }, @@ -1609,20 +1613,32 @@ func TestFormatCompactEntry(t *testing.T) { wantMark: "✓ 1.0s", // exec keeps duration }, { - name: "exec long entry truncated from end", - entry: toolLogEntry{Name: "[2] exec", ArgsSnip: "pytest tests/integration/test_very_long_name.py", Result: "✗ 3.0s"}, + name: "exec long entry truncated from end", + entry: toolLogEntry{ + Name: "[2] exec", + ArgsSnip: "pytest tests/integration/test_very_long_name.py", + Result: "✗ 3.0s", + }, wantMark: "✗", }, { - name: "file tool omits duration, shows filename", - entry: toolLogEntry{Name: "[3] edit_file", ArgsSnip: "projects/terra/src/deep/nested/backend.py", Result: "✓ 0.0s"}, + name: "file tool omits duration, shows filename", + entry: toolLogEntry{ + Name: "[3] edit_file", + ArgsSnip: "projects/terra/src/deep/nested/backend.py", + Result: "✓ 0.0s", + }, wantSub: "backend.py", wantMark: "✓", noTime: true, }, { - name: "file tool path truncates from start", - entry: toolLogEntry{Name: "[4] read_file", ArgsSnip: "projects/terra-py-form/src/terra_py_form/hot/state/backend.py", Result: "✓ 0.1s"}, + name: "file tool path truncates from start", + entry: toolLogEntry{ + Name: "[4] read_file", + ArgsSnip: "projects/terra-py-form/src/terra_py_form/hot/state/backend.py", + Result: "✓ 0.1s", + }, wantSub: "backend.py", wantMark: "✓", noTime: true, @@ -1745,7 +1761,7 @@ func TestExtractExecProjectDir(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - args := map[string]interface{}{"command": tt.cmd} + args := map[string]any{"command": tt.cmd} got := extractExecProjectDir(args) if got != tt.want { t.Errorf("extractExecProjectDir(%q) = %q, want %q", tt.cmd, got, tt.want) diff --git a/pkg/agent/memory.go b/pkg/agent/memory.go index 285cf895b..ce6147e25 100644 --- a/pkg/agent/memory.go +++ b/pkg/agent/memory.go @@ -520,12 +520,22 @@ func (ms *MemoryStore) getInterviewContextFrom(content string) string { sb.WriteString("- Tooling preferences (test framework, linter, formatter, CI)\n") sb.WriteString("- Key commands the user already runs (build, test, deploy)\n") sb.WriteString("\n### Rules\n") - sb.WriteString("- NEVER remove or overwrite the header block (`# Active Plan`, `> Task:`, `> Status:`, `> Phase:` lines). The system parses these to track state.\n") - sb.WriteString("- After each answer, use edit_file to append findings to the ## Context section of memory/MEMORY.md.\n") - sb.WriteString("- When you have enough information, use edit_file to add ## Phase, ## Commands, and ## Context sections BELOW the header block.\n") - sb.WriteString("- Each step MUST use checkbox syntax: `- [ ] description`. The system parses checkboxes to track progress.\n") + sb.WriteString( + "- NEVER remove or overwrite the header block (`# Active Plan`, `> Task:`, `> Status:`, `> Phase:` lines). The system parses these to track state.\n", + ) + sb.WriteString( + "- After each answer, use edit_file to append findings to the ## Context section of memory/MEMORY.md.\n", + ) + sb.WriteString( + "- When you have enough information, use edit_file to add ## Phase, ## Commands, and ## Context sections BELOW the header block.\n", + ) + sb.WriteString( + "- Each step MUST use checkbox syntax: `- [ ] description`. The system parses checkboxes to track progress.\n", + ) sb.WriteString("- Organize into 2-5 phases with 3-5 steps each.\n") - sb.WriteString("- After writing Phases, change `> Status: interviewing` to `> Status: review` via edit_file. The user must approve with /plan start before execution begins.\n") + sb.WriteString( + "- After writing Phases, change `> Status: interviewing` to `> Status: review` via edit_file. The user must approve with /plan start before execution begins.\n", + ) sb.WriteString("\n### Target Format (MANDATORY — system parses this exact structure)\n") sb.WriteString("\n") sb.WriteString("# Active Plan\n") diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 41b7d0706..4a64bac72 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -435,7 +435,8 @@ func TestAgentDefaults_PlanModel_StringParse(t *testing.T) { if cfg.Agents.Defaults.PlanModel != "anthropic/claude-sonnet-4-6" { t.Errorf("PlanModel = %q, want 'anthropic/claude-sonnet-4-6'", cfg.Agents.Defaults.PlanModel) } - if len(cfg.Agents.Defaults.PlanModelFallbacks) != 1 || cfg.Agents.Defaults.PlanModelFallbacks[0] != "openai/gpt-4o" { + if len(cfg.Agents.Defaults.PlanModelFallbacks) != 1 || + cfg.Agents.Defaults.PlanModelFallbacks[0] != "openai/gpt-4o" { t.Errorf("PlanModelFallbacks = %v, want [openai/gpt-4o]", cfg.Agents.Defaults.PlanModelFallbacks) } } diff --git a/pkg/git/worktree_test.go b/pkg/git/worktree_test.go index 09d01cbc6..41d2d97aa 100644 --- a/pkg/git/worktree_test.go +++ b/pkg/git/worktree_test.go @@ -17,7 +17,10 @@ func TestSanitizeBranchName(t *testing.T) { {" spaces ", "plan/spaces"}, {"UPPER-case_Mix", "plan/upper-case-mix"}, {"a/b/c", "plan/a-b-c"}, - {"very long task name that exceeds the forty character limit for safety", "plan/very-long-task-name-that-exceeds-the-for"}, + { + "very long task name that exceeds the forty character limit for safety", + "plan/very-long-task-name-that-exceeds-the-for", + }, {"---leading-trailing---", "plan/leading-trailing"}, {"special!@#$%chars", "plan/special-chars"}, } diff --git a/pkg/logger/logger_test.go b/pkg/logger/logger_test.go index 958c70a59..741b61209 100644 --- a/pkg/logger/logger_test.go +++ b/pkg/logger/logger_test.go @@ -310,13 +310,26 @@ func TestSanitizeFields(t *testing.T) { maskedK: nil, }, { - name: "sensitive keys masked", - input: map[string]any{"token": "abc123", "api_key": "sk-xxx", "secret": "s3cr3t", "password": "pass", "authorization": "Bearer tok"}, + name: "sensitive keys masked", + input: map[string]any{ + "token": "abc123", + "api_key": "sk-xxx", + "secret": "s3cr3t", + "password": "pass", + "authorization": "Bearer tok", + }, maskedK: []string{"token", "api_key", "secret", "password", "authorization"}, }, { - name: "case insensitive", - input: map[string]any{"Token": "abc", "API_KEY": "xyz", "Secret": "s", "PASSWORD": "p", "Authorization": "a", "Credential": "c"}, + name: "case insensitive", + input: map[string]any{ + "Token": "abc", + "API_KEY": "xyz", + "Secret": "s", + "PASSWORD": "p", + "Authorization": "a", + "Credential": "c", + }, maskedK: []string{"Token", "API_KEY", "Secret", "PASSWORD", "Authorization", "Credential"}, }, { @@ -325,8 +338,13 @@ func TestSanitizeFields(t *testing.T) { safeK: []string{"error", "count", "user_id", "component"}, }, { - name: "mixed keys", - input: map[string]any{"token": "sensitive", "msg_signature": "safe", "corp_secret": "sensitive2", "nonce": "safe2"}, + name: "mixed keys", + input: map[string]any{ + "token": "sensitive", + "msg_signature": "safe", + "corp_secret": "sensitive2", + "nonce": "safe2", + }, maskedK: []string{"token", "corp_secret"}, safeK: []string{"msg_signature", "nonce"}, }, diff --git a/pkg/miniapp/api.go b/pkg/miniapp/api.go index c8fd33adb..a385c8a54 100644 --- a/pkg/miniapp/api.go +++ b/pkg/miniapp/api.go @@ -134,7 +134,13 @@ func (h *Handler) apiEvents(w http.ResponseWriter, r *http.Request) { sendSSEIfChanged(w, flusher, "skills", h.provider.ListSkills(), &lastSkills) sendSSEIfChanged(w, flusher, "dev", h.devStatus(), &lastDev) sendSSEIfChanged(w, flusher, "context", h.provider.GetContextInfo(), &lastContext) - sendSSEIfChanged(w, flusher, "prompt", map[string]string{"prompt": h.provider.GetSystemPrompt()}, &lastPrompt) + sendSSEIfChanged( + w, + flusher, + "prompt", + map[string]string{"prompt": h.provider.GetSystemPrompt()}, + &lastPrompt, + ) } } } diff --git a/pkg/miniapp/miniapp.go b/pkg/miniapp/miniapp.go index c6d71fbfc..1a0e05e04 100644 --- a/pkg/miniapp/miniapp.go +++ b/pkg/miniapp/miniapp.go @@ -40,7 +40,14 @@ type Handler struct { } // NewHandler creates a new Mini App handler. -func NewHandler(provider DataProvider, sender CommandSender, botToken string, notifier *StateNotifier, allowList []string, workspace string) *Handler { +func NewHandler( + provider DataProvider, + sender CommandSender, + botToken string, + notifier *StateNotifier, + allowList []string, + workspace string, +) *Handler { return &Handler{ provider: provider, sender: sender, diff --git a/pkg/providers/claude_cli_provider_test.go b/pkg/providers/claude_cli_provider_test.go index a89047df6..b57f89fd2 100644 --- a/pkg/providers/claude_cli_provider_test.go +++ b/pkg/providers/claude_cli_provider_test.go @@ -1086,7 +1086,7 @@ func TestExtractXMLToolCalls_MismatchedCloseTag(t *testing.T) { } } -func TestStripXMLToolCalls_MismatchedCloseTag(t *testing.T) { +func TestStripXMLToolCalls_MismatchedCloseTag(t *testing.T) { //nolint:gosmopolitan // CJK test data text := `今テスト走らせるね。 diff --git a/pkg/providers/http_provider.go b/pkg/providers/http_provider.go index 1f02e9e1b..49f45938b 100644 --- a/pkg/providers/http_provider.go +++ b/pkg/providers/http_provider.go @@ -48,7 +48,13 @@ func NewHTTPProviderWithOptions(apiKey, apiBase, proxy string, opts ...openai_co } } -func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) { +func (p *HTTPProvider) Chat( + ctx context.Context, + messages []Message, + tools []ToolDefinition, + model string, + options map[string]any, +) (*LLMResponse, error) { resp, err := p.delegate.Chat(ctx, messages, tools, model, options) if err != nil { return nil, err diff --git a/pkg/providers/openai_compat/provider.go b/pkg/providers/openai_compat/provider.go index cc054753e..df26c7bb4 100644 --- a/pkg/providers/openai_compat/provider.go +++ b/pkg/providers/openai_compat/provider.go @@ -554,7 +554,17 @@ func normalizeModel(model, apiBase string) string { prefix := strings.ToLower(model[:idx]) switch prefix { - case "openai", "moonshot", "nvidia", "groq", "ollama", "deepseek", "google", "openrouter", "zhipu", "minimax", "mistral": + case "openai", + "moonshot", + "nvidia", + "groq", + "ollama", + "deepseek", + "google", + "openrouter", + "zhipu", + "minimax", + "mistral": return model[idx+1:] default: return model diff --git a/pkg/providers/tool_call_extract.go b/pkg/providers/tool_call_extract.go index 770e1410a..44f2403cb 100644 --- a/pkg/providers/tool_call_extract.go +++ b/pkg/providers/tool_call_extract.go @@ -177,7 +177,8 @@ func findToolCallBlock(text string) (blockStart, blockEnd int, content string, f } // Also consume a preceding [TOOLCALL] marker if present. start := invokePos - if loc := reBracketMarker.FindStringIndex(before[:start]); loc != nil && strings.TrimSpace(before[loc[1]:start]) == "" { + if loc := reBracketMarker.FindStringIndex(before[:start]); loc != nil && + strings.TrimSpace(before[loc[1]:start]) == "" { start = loc[0] } return start, cm[1], text[invokePos:cm[0]], true @@ -245,7 +246,7 @@ func parseInvokeElements(text string, callIdx *int) []ToolCall { toolName := invokeBody[nameStart : nameStart+nameEnd] // Extract parameters - args := make(map[string]interface{}) + args := make(map[string]any) paramRemaining := invokeBody for { pStart := strings.Index(paramRemaining, "= bgMaxProcesses { t.bgMu.Unlock() - return ErrorResult(fmt.Sprintf("maximum background processes reached (%d). Kill an existing one first.", bgMaxProcesses)) + return ErrorResult( + fmt.Sprintf("maximum background processes reached (%d). Kill an existing one first.", bgMaxProcesses), + ) } t.bgNextID++ diff --git a/pkg/tools/subagent_reporter_test.go b/pkg/tools/subagent_reporter_test.go index 9cbec59ab..8906a5453 100644 --- a/pkg/tools/subagent_reporter_test.go +++ b/pkg/tools/subagent_reporter_test.go @@ -21,7 +21,13 @@ func newBlockingProvider() *blockingProvider { return &blockingProvider{ready: make(chan struct{})} } -func (p *blockingProvider) Chat(ctx context.Context, _ []providers.Message, _ []providers.ToolDefinition, _ string, _ map[string]any) (*providers.LLMResponse, error) { +func (p *blockingProvider) Chat( + ctx context.Context, + _ []providers.Message, + _ []providers.ToolDefinition, + _ string, + _ map[string]any, +) (*providers.LLMResponse, error) { close(p.ready) // signal: we are now blocking <-ctx.Done() return nil, ctx.Err()