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 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-28 23:53:23 +09:00
parent 5801176dfe
commit dca46e6799
16 changed files with 235 additions and 93 deletions

View file

@ -205,7 +205,11 @@ func gatewayCmd(debug bool, orchestration bool) error {
// Auto-detect Tailscale hostname and fetch TLS cert // Auto-detect Tailscale hostname and fetch TLS cert
hostname, tsErr := tailscale.DetectHostname() hostname, tsErr := tailscale.DetectHostname()
if tsErr != nil { 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 { } else {
certDir := filepath.Join(cfg.WorkspacePath(), "state", "certs") certDir := filepath.Join(cfg.WorkspacePath(), "state", "certs")
certFile, keyFile, certErr := tailscale.FetchCert(hostname, certDir) 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()} dataProvider := &agentLoopDataProvider{loop: agentLoop, workspace: cfg.WorkspacePath()}
sender := &telegramCommandSender{bus: msgBus} sender := &telegramCommandSender{bus: msgBus}
miniappNotifier = miniapp.NewStateNotifier() 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 agentLoop.OnStateChange = miniappNotifier.Notify
if b := agentLoop.GetOrchBroadcaster(); b != nil { if b := agentLoop.GetOrchBroadcaster(); b != nil {
handler.SetOrchBroadcaster(b) handler.SetOrchBroadcaster(b)
@ -251,7 +262,11 @@ func gatewayCmd(debug bool, orchestration bool) error {
} }
}() }()
if useTLS { 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 { } else {
fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port) fmt.Printf("✓ Health endpoints available at http://%s:%d/health and /ready\n", cfg.Gateway.Host, cfg.Gateway.Port)
} }

View file

@ -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." 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) registry := NewAgentRegistry(cfg, provider)
// Set up shared fallback chain // Set up shared fallback chain
@ -282,7 +287,14 @@ func registerSharedTools(
PerplexityMaxResults: cfg.Tools.Web.Perplexity.MaxResults, PerplexityMaxResults: cfg.Tools.Web.Perplexity.MaxResults,
PerplexityEnabled: cfg.Tools.Web.Perplexity.Enabled, 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) subagentManager.SetLLMOptions(agent.MaxTokens, agent.Temperature)
spawnTool := tools.NewSpawnTool(subagentManager) spawnTool := tools.NewSpawnTool(subagentManager)
currentAgentID := agentID 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 // 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 // only resolved once. Looks up model_list first (new format), then falls back
// to the legacy providers section via CreateProviderByName. // 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) key := strings.ToLower(providerName + "/" + modelName)
if key == "/" { if key == "/" {
return fallback return fallback
@ -497,14 +512,14 @@ func (al *AgentLoop) resolveProvider(providerName, modelName string, fallback pr
return p return p
} }
logger.WarnCF("agent", "Failed to create provider from model_list, trying legacy", 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. // Fall back to legacy providers section.
p, err := providers.CreateProviderByName(al.cfg, providerName) p, err := providers.CreateProviderByName(al.cfg, providerName)
if err != nil { if err != nil {
logger.WarnCF("agent", "Failed to create provider for fallback, using primary", 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 return fallback
} }
al.providerCache[key] = p al.providerCache[key] = p
@ -630,7 +645,7 @@ func (al *AgentLoop) processMessage(ctx context.Context, msg bus.InboundMessage)
lower := strings.ToLower(content) lower := strings.ToLower(content)
// Check for stop keywords // Check for stop keywords
stopKeywords := []string{"stop", "cancel", "abort", "停止", "中止", "やめて"} stopKeywords := []string{"stop", "cancel", "abort", "停止", "中止", "やめて"} //nolint:gosmopolitan // intentional CJK stop words
isStop := false isStop := false
for _, kw := range stopKeywords { for _, kw := range stopKeywords {
if lower == kw { 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. // 1a. Set session-specific working directory for bootstrap file lookup.
// Prefer the tool-detected project directory (touch_dir) from the session tracker, // 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. // 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)) agent.ContextBuilder.SetWorkDir(filepath.Join(agent.Workspace, active[0].TouchDir))
} else { } else {
agent.ContextBuilder.SetWorkDir(agent.EffectiveWorkspace(opts.SessionKey)) 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("\n\n## Background Execution\n")
sb.WriteString("You are running as a background heartbeat with no conversation history. ") 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("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() 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 // 5a. Auto-advance plan phases after LLM iteration
postStatus := agent.ContextBuilder.GetPlanStatus() 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 // Intercept: if AI changed status to executing or review without user approval
// (from interviewing or review), validate and hold at "review". // (from interviewing or review), validate and hold at "review".
if preStatus == "interviewing" || (preStatus == "review" && postStatus == "executing") { if preStatus == "interviewing" || (preStatus == "review" && postStatus == "executing") {
if err := agent.ContextBuilder.ValidatePlanStructure(); err != nil { if err := agent.ContextBuilder.ValidatePlanStructure(); err != nil {
_ = agent.ContextBuilder.SetPlanStatus("interviewing") _ = agent.ContextBuilder.SetPlanStatus("interviewing")
logger.WarnCF("agent", "Reverted plan to interviewing: "+err.Error(), 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 // Inject rejection into session history so LLM sees it next iteration
rejectionMsg := "[System] Plan rejected: " + err.Error() + ". Fix and try again." rejectionMsg := "[System] Plan rejected: " + err.Error() + ". Fix and try again."
agent.Sessions.AddMessage(opts.SessionKey, "user", rejectionMsg) 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). // Safeguard: executing but no phases (shouldn't happen, but be safe).
_ = agent.ContextBuilder.SetPlanStatus("interviewing") _ = agent.ContextBuilder.SetPlanStatus("interviewing")
logger.WarnCF("agent", "Reverted plan to interviewing: no phases defined", 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() { } else if agent.ContextBuilder.IsPlanComplete() {
// Mark plan as completed (keep memory for review; user can /plan clear) // Mark plan as completed (keep memory for review; user can /plan clear)
total := agent.ContextBuilder.GetTotalPhases() total := agent.ContextBuilder.GetTotalPhases()
@ -1194,7 +1213,8 @@ func buildTaskReminder(userMessage string, lastBlocker string) providers.Message
truncatedBlocker := utils.Truncate(lastBlocker, blockerMaxChars) truncatedBlocker := utils.Truncate(lastBlocker, blockerMaxChars)
content = fmt.Sprintf( 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.", "[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 { } else {
content = fmt.Sprintf( content = fmt.Sprintf(
@ -1247,7 +1267,7 @@ var optFlagPattern = regexp.MustCompile(`\s+--?\w[\w-]*(=\S*)?`)
// extractExecProjectDir extracts the basename of an exec cd target. // extractExecProjectDir extracts the basename of an exec cd target.
// Returns "" if the command has no cd prefix. // 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) cmd, _ := args["command"].(string)
if cmd == "" { if cmd == "" {
return "" return ""
@ -1338,7 +1358,7 @@ func (al *AgentLoop) targetReasoningChannelID(channelName string) (chatID string
// For exec: extracts the command and strips the leading "cd <workspace> && ". // For exec: extracts the command and strips the leading "cd <workspace> && ".
// For file tools: extracts the path and strips the workspace prefix. // For file tools: extracts the path and strips the workspace prefix.
// Falls back to raw JSON truncation. // 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 { switch toolName {
case "exec": case "exec":
cmd, _ := args["command"].(string) cmd, _ := args["command"].(string)
@ -1829,7 +1849,12 @@ type streamToolCallAcc struct {
} }
// buildAccumulatedResponse constructs an LLMResponse from accumulated stream data. // 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{ resp := &providers.LLMResponse{
Content: content, Content: content,
Reasoning: reasoning, Reasoning: reasoning,
@ -2426,7 +2451,14 @@ func (al *AgentLoop) runLLMIteration(
if wt := agent.GetWorktree(opts.SessionKey); wt != nil { if wt := agent.GetWorktree(opts.SessionKey); wt != nil {
toolCtx = tools.WithWorkspaceOverride(toolCtx, wt.Path) 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) toolDuration := time.Since(toolStart)
// Update tool log entry with result // Update tool log entry with result
@ -2539,7 +2571,7 @@ func (al *AgentLoop) runLLMIteration(
messages = append(messages, reminderMsg) messages = append(messages, reminderMsg)
lastReminderIdx = len(messages) - 1 lastReminderIdx = len(messages) - 1
logger.DebugCF("agent", "Injected task reminder", logger.DebugCF("agent", "Injected task reminder",
map[string]interface{}{ map[string]any{
"agent_id": agent.ID, "agent_id": agent.ID,
"iteration": iteration, "iteration": iteration,
"has_blocker": lastBlocker != "", "has_blocker": lastBlocker != "",
@ -2551,7 +2583,7 @@ func (al *AgentLoop) runLLMIteration(
if reminder, ok := buildPlanReminder(planSnapshot); ok { if reminder, ok := buildPlanReminder(planSnapshot); ok {
messages = append(messages, reminder) messages = append(messages, reminder)
logger.DebugCF("agent", "Injected plan reminder", logger.DebugCF("agent", "Injected plan reminder",
map[string]interface{}{ map[string]any{
"agent_id": agent.ID, "agent_id": agent.ID,
"iteration": iteration, "iteration": iteration,
"plan_status": planSnapshot, "plan_status": planSnapshot,
@ -2565,7 +2597,8 @@ func (al *AgentLoop) runLLMIteration(
if touchDir := al.sessions.GetTouchDir(opts.SessionKey); touchDir != "" { if touchDir := al.sessions.GetTouchDir(opts.SessionKey); touchDir != "" {
agent.ContextBuilder.SetWorkDir(filepath.Join(agent.Workspace, 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 messages[0].Content = newPrompt
al.lastSystemPrompt.Store(newPrompt) al.lastSystemPrompt.Store(newPrompt)
al.promptDirty.Store(false) al.promptDirty.Store(false)
@ -2577,11 +2610,11 @@ func (al *AgentLoop) runLLMIteration(
// make one final LLM call without tools to force a text response. // make one final LLM call without tools to force a text response.
if finalContent == "" && iteration >= maxIter { if finalContent == "" && iteration >= maxIter {
logger.WarnCF("agent", "Max iterations reached, forcing final response without tools", logger.WarnCF("agent", "Max iterations reached, forcing final response without tools",
map[string]interface{}{ map[string]any{
"agent_id": agent.ID, "agent_id": agent.ID,
"iteration": iteration, "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, "max_tokens": agent.MaxTokens,
"temperature": agent.Temperature, "temperature": agent.Temperature,
"prompt_cache_key": agent.ID, "prompt_cache_key": agent.ID,
@ -2633,7 +2666,7 @@ func (al *AgentLoop) maybeSummarize(agent *AgentInstance, sessionKey, channel, c
go func() { go func() {
defer al.summarizing.Delete(summarizeKey) defer al.summarizing.Delete(summarizeKey)
logger.InfoCF("agent", "Memory threshold reached, optimizing conversation history", logger.InfoCF("agent", "Memory threshold reached, optimizing conversation history",
map[string]interface{}{ map[string]any{
"session_key": sessionKey, "session_key": sessionKey,
"history_len": len(newHistory), "history_len": len(newHistory),
"token_estimate": tokenEstimate, "token_estimate": tokenEstimate,
@ -3127,7 +3160,8 @@ func (al *AgentLoop) handleSessionCommand(args []string) string {
} }
s := al.stats.GetStats() 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.Date,
s.Today.Prompts, s.Today.Prompts,
s.Today.Requests, 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 // 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 // name-level gating, then applies argument-level constraints for write-type tools
// (MEMORY.md only) and exec (read-only commands only). // (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) norm := tools.NormalizeToolName(toolName)
if !interviewAllowedTools[norm] { if !interviewAllowedTools[norm] {
return false return false

View file

@ -1466,7 +1466,7 @@ Test
func TestIsToolAllowedDuringInterview_FuzzyNames(t *testing.T) { func TestIsToolAllowedDuringInterview_FuzzyNames(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
args map[string]interface{} args map[string]any
want bool want bool
}{ }{
// Exact names — read tools allowed // Exact names — read tools allowed
@ -1484,38 +1484,38 @@ func TestIsToolAllowedDuringInterview_FuzzyNames(t *testing.T) {
{"message", nil, true}, {"message", nil, true},
{"Message", nil, true}, {"Message", nil, true},
// Write to MEMORY.md — allowed // Write to MEMORY.md — allowed
{"edit_file", map[string]interface{}{"path": "/ws/memory/MEMORY.md"}, true}, {"edit_file", map[string]any{"path": "/ws/memory/MEMORY.md"}, true},
{"editfile", map[string]interface{}{"path": "/ws/memory/MEMORY.md"}, true}, {"editfile", map[string]any{"path": "/ws/memory/MEMORY.md"}, true},
{"EditFile", map[string]interface{}{"path": "/ws/memory/MEMORY.md"}, true}, {"EditFile", map[string]any{"path": "/ws/memory/MEMORY.md"}, true},
// Write to non-MEMORY.md — blocked // Write to non-MEMORY.md — blocked
{"edit_file", map[string]interface{}{"path": "/ws/main.go"}, false}, {"edit_file", map[string]any{"path": "/ws/main.go"}, false},
{"editfile", map[string]interface{}{"path": "/ws/main.go"}, false}, {"editfile", map[string]any{"path": "/ws/main.go"}, false},
// exec — read-only commands allowed // exec — read-only commands allowed
{"exec", map[string]interface{}{"command": "find . -name '*.py'"}, true}, {"exec", map[string]any{"command": "find . -name '*.py'"}, true},
{"exec", map[string]interface{}{"command": "ls -la"}, true}, {"exec", map[string]any{"command": "ls -la"}, true},
{"exec", map[string]interface{}{"command": "grep -r TODO ."}, true}, {"exec", map[string]any{"command": "grep -r TODO ."}, true},
{"exec", map[string]interface{}{"command": "cat README.md"}, true}, {"exec", map[string]any{"command": "cat README.md"}, true},
// exec — cd prefix stripped // exec — cd prefix stripped
{"exec", map[string]interface{}{"command": "cd /home/user/project && find . -type f"}, true}, {"exec", map[string]any{"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 /tmp && rm -rf *"}, false},
// exec — write operators blocked // exec — write operators blocked
{"exec", map[string]interface{}{"command": "find . > output.txt"}, false}, {"exec", map[string]any{"command": "find . > output.txt"}, false},
{"exec", map[string]interface{}{"command": "ls -la >> log.txt"}, false}, {"exec", map[string]any{"command": "ls -la >> log.txt"}, false},
{"exec", map[string]interface{}{"command": "cat foo | tee bar.txt"}, false}, {"exec", map[string]any{"command": "cat foo | tee bar.txt"}, false},
// exec — path traversal blocked // exec — path traversal blocked
{"exec", map[string]interface{}{"command": "cat ../../etc/passwd"}, false}, {"exec", map[string]any{"command": "cat ../../etc/passwd"}, false},
{"exec", map[string]interface{}{"command": "find ../../"}, false}, {"exec", map[string]any{"command": "find ../../"}, false},
{"exec", map[string]interface{}{"command": "ls ../secret"}, false}, {"exec", map[string]any{"command": "ls ../secret"}, false},
// exec — absolute paths blocked // exec — absolute paths blocked
{"exec", map[string]interface{}{"command": "cat /etc/passwd"}, false}, {"exec", map[string]any{"command": "cat /etc/passwd"}, false},
{"exec", map[string]interface{}{"command": "find /etc -name '*.conf'"}, false}, {"exec", map[string]any{"command": "find /etc -name '*.conf'"}, false},
{"exec", map[string]interface{}{"command": "ls /root"}, false}, {"exec", map[string]any{"command": "ls /root"}, false},
// exec — write commands blocked // exec — write commands blocked
{"exec", map[string]interface{}{"command": "rm -rf /"}, false}, {"exec", map[string]any{"command": "rm -rf /"}, false},
{"exec", map[string]interface{}{"command": "mv a b"}, false}, {"exec", map[string]any{"command": "mv a b"}, false},
// exec — no args / empty command blocked // exec — no args / empty command blocked
{"exec", nil, false}, {"exec", nil, false},
{"exec", map[string]interface{}{"command": ""}, false}, {"exec", map[string]any{"command": ""}, false},
{"Exec", nil, false}, {"Exec", nil, false},
} }
for _, tt := range tests { for _, tt := range tests {
@ -1530,56 +1530,60 @@ func TestBuildArgsSnippet_ExecStripsCD(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
tool string tool string
args map[string]interface{} args map[string]any
workspace string workspace string
wantSnip string wantSnip string
}{ }{
{ {
name: "exec strips cd prefix", name: "exec strips cd prefix",
tool: "exec", tool: "exec",
args: map[string]interface{}{"command": "cd /home/user/workspace/project/my-projects && pytest tests/test_integration.py"}, args: map[string]any{
"command": "cd /home/user/workspace/project/my-projects && pytest tests/test_integration.py",
},
workspace: "/home/user/workspace", workspace: "/home/user/workspace",
wantSnip: "pytest tests/test_integration.py", wantSnip: "pytest tests/test_integration.py",
}, },
{ {
name: "exec no cd prefix, flags stripped", name: "exec no cd prefix, flags stripped",
tool: "exec", tool: "exec",
args: map[string]interface{}{"command": "ls -la"}, args: map[string]any{"command": "ls -la"},
workspace: "/ws", workspace: "/ws",
wantSnip: "ls", wantSnip: "ls",
}, },
{ {
name: "exec empty command", name: "exec empty command",
tool: "exec", tool: "exec",
args: map[string]interface{}{}, args: map[string]any{},
workspace: "/ws", workspace: "/ws",
wantSnip: "{}", wantSnip: "{}",
}, },
{ {
name: "read_file strips workspace", name: "read_file strips workspace",
tool: "read_file", 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", workspace: "/home/user/workspace",
wantSnip: "src/main.go", wantSnip: "src/main.go",
}, },
{ {
name: "edit_file shows path", name: "edit_file shows path",
tool: "edit_file", 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", workspace: "/ws",
wantSnip: "config.json", wantSnip: "config.json",
}, },
{ {
name: "file tool long path prioritizes filename", name: "file tool long path prioritizes filename",
tool: "read_file", tool: "read_file",
args: map[string]interface{}{"path": "/ws/projects/terra-py-form/src/terra_py_form/hot/state/backend.py"}, args: map[string]any{
"path": "/ws/projects/terra-py-form/src/terra_py_form/hot/state/backend.py",
},
workspace: "/ws", workspace: "/ws",
wantSnip: "projects/terra-py-form/src/terra_py_form/hot/sta\u2026/backend.py", wantSnip: "projects/terra-py-form/src/terra_py_form/hot/sta\u2026/backend.py",
}, },
{ {
name: "unknown tool shows raw JSON", name: "unknown tool shows raw JSON",
tool: "web_search", tool: "web_search",
args: map[string]interface{}{"query": "hello"}, args: map[string]any{"query": "hello"},
workspace: "/ws", workspace: "/ws",
wantSnip: `{"query":"hello"}`, wantSnip: `{"query":"hello"}`,
}, },
@ -1610,19 +1614,31 @@ func TestFormatCompactEntry(t *testing.T) {
}, },
{ {
name: "exec long entry truncated from end", name: "exec long entry truncated from end",
entry: toolLogEntry{Name: "[2] exec", ArgsSnip: "pytest tests/integration/test_very_long_name.py", Result: "✗ 3.0s"}, entry: toolLogEntry{
Name: "[2] exec",
ArgsSnip: "pytest tests/integration/test_very_long_name.py",
Result: "✗ 3.0s",
},
wantMark: "✗", wantMark: "✗",
}, },
{ {
name: "file tool omits duration, shows filename", name: "file tool omits duration, shows filename",
entry: toolLogEntry{Name: "[3] edit_file", ArgsSnip: "projects/terra/src/deep/nested/backend.py", Result: "✓ 0.0s"}, entry: toolLogEntry{
Name: "[3] edit_file",
ArgsSnip: "projects/terra/src/deep/nested/backend.py",
Result: "✓ 0.0s",
},
wantSub: "backend.py", wantSub: "backend.py",
wantMark: "✓", wantMark: "✓",
noTime: true, noTime: true,
}, },
{ {
name: "file tool path truncates from start", 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"}, 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", wantSub: "backend.py",
wantMark: "✓", wantMark: "✓",
noTime: true, noTime: true,
@ -1745,7 +1761,7 @@ func TestExtractExecProjectDir(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) got := extractExecProjectDir(args)
if got != tt.want { if got != tt.want {
t.Errorf("extractExecProjectDir(%q) = %q, want %q", tt.cmd, got, tt.want) t.Errorf("extractExecProjectDir(%q) = %q, want %q", tt.cmd, got, tt.want)

View file

@ -520,12 +520,22 @@ func (ms *MemoryStore) getInterviewContextFrom(content string) string {
sb.WriteString("- Tooling preferences (test framework, linter, formatter, CI)\n") sb.WriteString("- Tooling preferences (test framework, linter, formatter, CI)\n")
sb.WriteString("- Key commands the user already runs (build, test, deploy)\n") sb.WriteString("- Key commands the user already runs (build, test, deploy)\n")
sb.WriteString("\n### Rules\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(
sb.WriteString("- After each answer, use edit_file to append findings to the ## Context section of memory/MEMORY.md.\n") "- NEVER remove or overwrite the header block (`# Active Plan`, `> Task:`, `> Status:`, `> Phase:` lines). The system parses these to track state.\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(
"- 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("- 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### Target Format (MANDATORY — system parses this exact structure)\n")
sb.WriteString("\n") sb.WriteString("\n")
sb.WriteString("# Active Plan\n") sb.WriteString("# Active Plan\n")

View file

@ -435,7 +435,8 @@ func TestAgentDefaults_PlanModel_StringParse(t *testing.T) {
if cfg.Agents.Defaults.PlanModel != "anthropic/claude-sonnet-4-6" { if cfg.Agents.Defaults.PlanModel != "anthropic/claude-sonnet-4-6" {
t.Errorf("PlanModel = %q, want 'anthropic/claude-sonnet-4-6'", cfg.Agents.Defaults.PlanModel) 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) t.Errorf("PlanModelFallbacks = %v, want [openai/gpt-4o]", cfg.Agents.Defaults.PlanModelFallbacks)
} }
} }

View file

@ -17,7 +17,10 @@ func TestSanitizeBranchName(t *testing.T) {
{" spaces ", "plan/spaces"}, {" spaces ", "plan/spaces"},
{"UPPER-case_Mix", "plan/upper-case-mix"}, {"UPPER-case_Mix", "plan/upper-case-mix"},
{"a/b/c", "plan/a-b-c"}, {"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"}, {"---leading-trailing---", "plan/leading-trailing"},
{"special!@#$%chars", "plan/special-chars"}, {"special!@#$%chars", "plan/special-chars"},
} }

View file

@ -311,12 +311,25 @@ func TestSanitizeFields(t *testing.T) {
}, },
{ {
name: "sensitive keys masked", name: "sensitive keys masked",
input: map[string]any{"token": "abc123", "api_key": "sk-xxx", "secret": "s3cr3t", "password": "pass", "authorization": "Bearer tok"}, 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"}, maskedK: []string{"token", "api_key", "secret", "password", "authorization"},
}, },
{ {
name: "case insensitive", name: "case insensitive",
input: map[string]any{"Token": "abc", "API_KEY": "xyz", "Secret": "s", "PASSWORD": "p", "Authorization": "a", "Credential": "c"}, 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"}, maskedK: []string{"Token", "API_KEY", "Secret", "PASSWORD", "Authorization", "Credential"},
}, },
{ {
@ -326,7 +339,12 @@ func TestSanitizeFields(t *testing.T) {
}, },
{ {
name: "mixed keys", name: "mixed keys",
input: map[string]any{"token": "sensitive", "msg_signature": "safe", "corp_secret": "sensitive2", "nonce": "safe2"}, input: map[string]any{
"token": "sensitive",
"msg_signature": "safe",
"corp_secret": "sensitive2",
"nonce": "safe2",
},
maskedK: []string{"token", "corp_secret"}, maskedK: []string{"token", "corp_secret"},
safeK: []string{"msg_signature", "nonce"}, safeK: []string{"msg_signature", "nonce"},
}, },

View file

@ -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, "skills", h.provider.ListSkills(), &lastSkills)
sendSSEIfChanged(w, flusher, "dev", h.devStatus(), &lastDev) sendSSEIfChanged(w, flusher, "dev", h.devStatus(), &lastDev)
sendSSEIfChanged(w, flusher, "context", h.provider.GetContextInfo(), &lastContext) 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,
)
} }
} }
} }

View file

@ -40,7 +40,14 @@ type Handler struct {
} }
// NewHandler creates a new Mini App handler. // 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{ return &Handler{
provider: provider, provider: provider,
sender: sender, sender: sender,

View file

@ -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 := `今テスト走らせるね text := `今テスト走らせるね
<minimax:toolcall> <minimax:toolcall>
<invoke name="exec"> <invoke name="exec">

View file

@ -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) resp, err := p.delegate.Chat(ctx, messages, tools, model, options)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -554,7 +554,17 @@ func normalizeModel(model, apiBase string) string {
prefix := strings.ToLower(model[:idx]) prefix := strings.ToLower(model[:idx])
switch prefix { 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:] return model[idx+1:]
default: default:
return model return model

View file

@ -177,7 +177,8 @@ func findToolCallBlock(text string) (blockStart, blockEnd int, content string, f
} }
// Also consume a preceding [TOOLCALL] marker if present. // Also consume a preceding [TOOLCALL] marker if present.
start := invokePos 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] start = loc[0]
} }
return start, cm[1], text[invokePos:cm[0]], true 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] toolName := invokeBody[nameStart : nameStart+nameEnd]
// Extract parameters // Extract parameters
args := make(map[string]interface{}) args := make(map[string]any)
paramRemaining := invokeBody paramRemaining := invokeBody
for { for {
pStart := strings.Index(paramRemaining, "<parameter") pStart := strings.Index(paramRemaining, "<parameter")

View file

@ -74,7 +74,14 @@ func (t *DevPreviewTool) Execute(ctx context.Context, args map[string]any) *Tool
if err := t.manager.ActivateDevTarget(id); err != nil { if err := t.manager.ActivateDevTarget(id); err != nil {
return ErrorResult(fmt.Sprintf("failed to activate dev target: %v", err)) return ErrorResult(fmt.Sprintf("failed to activate dev target: %v", err))
} }
return SilentResult(fmt.Sprintf("Dev preview started (id=%s, name=%s). Target: %s\nUsers can view it in the Mini App Dev tab.", id, name, target)) return SilentResult(
fmt.Sprintf(
"Dev preview started (id=%s, name=%s). Target: %s\nUsers can view it in the Mini App Dev tab.",
id,
name,
target,
),
)
case "stop": case "stop":
if err := t.manager.DeactivateDevTarget(); err != nil { if err := t.manager.DeactivateDevTarget(); err != nil {

View file

@ -418,7 +418,9 @@ func (t *ExecTool) executeBg(command, cwd string) *ToolResult {
} }
if running >= bgMaxProcesses { if running >= bgMaxProcesses {
t.bgMu.Unlock() 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++ t.bgNextID++

View file

@ -21,7 +21,13 @@ func newBlockingProvider() *blockingProvider {
return &blockingProvider{ready: make(chan struct{})} 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 close(p.ready) // signal: we are now blocking
<-ctx.Done() <-ctx.Done()
return nil, ctx.Err() return nil, ctx.Err()