diff --git a/.kit/extensions/go-edit-lint.go b/.kit/extensions/go-edit-lint.go index 1a635aa6..84695460 100644 --- a/.kit/extensions/go-edit-lint.go +++ b/.kit/extensions/go-edit-lint.go @@ -28,11 +28,15 @@ type lintResult struct { Err error } +// Package-level state: set of .go files edited during the current agent turn. +var editedFiles map[string]bool + func Init(api ext.API) { api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) { - ctx.Print("go-edit-lint extension loaded - will run gopls and golangci-lint on Go file edits") + ctx.Print("go-edit-lint extension loaded - will run gopls and golangci-lint after agent turns that edit Go files") }) + // Track edited .go files — don't lint yet. api.OnToolResult(func(e ext.ToolResultEvent, ctx ext.Context) *ext.ToolResultResult { if e.IsError || !isEditOrWrite(e.ToolName) { return nil @@ -43,30 +47,72 @@ func Init(api ext.API) { return nil } - report := runGoDiagnostics(ctx.CWD, absPath) - - // Check if there are issues and add explicit prompt for the LLM to react - goplsIssues, lintIssues := countIssues(report) - hasIssues := goplsIssues > 0 || lintIssues > 0 - - var enhanced string - if hasIssues { - enhanced = e.Content + "\n\n" + report + "\n\n⚠️ DIAGNOSTICS FOUND: Please review the issues above and fix them before proceeding." - } else { - enhanced = e.Content + "\n\n" + report + if editedFiles == nil { + editedFiles = make(map[string]bool) + } + editedFiles[absPath] = true + return nil + }) + + // After the agent turn ends, lint all collected files. + api.OnAgentEnd(func(e ext.AgentEndEvent, ctx ext.Context) { + if len(editedFiles) == 0 { + return } - // Show TUI message block for diagnostics visibility (only if there are issues) + // Snapshot and reset immediately so the next turn starts clean. + files := editedFiles + editedFiles = nil + + // Skip lint on errored turns. + if e.StopReason == "error" { + return + } + + // Collect unique directories and file list for gopls. + var allGoplsOutput []string + for absPath := range files { + res := runGopls(ctx.CWD, absPath) + formatted := formatToolResult(res, "") + if formatted != "" { + allGoplsOutput = append(allGoplsOutput, fmt.Sprintf("# %s\n%s", filepath.Base(absPath), formatted)) + } + } + + lintRes := runGolangCILint(ctx.CWD, "./...") + + goplsSection := "No diagnostics." + if len(allGoplsOutput) > 0 { + goplsSection = strings.Join(allGoplsOutput, "\n\n") + } + lintSection := formatToolResult(lintRes, "No lint issues.") + + // Build file list for the report header. + var fileNames []string + for absPath := range files { + fileNames = append(fileNames, filepath.Base(absPath)) + } + + report := fmt.Sprintf( + "\n[gopls]\n%s\n\n[golangci-lint]\n%s\n", + strings.Join(fileNames, ", "), + goplsSection, + lintSection, + ) + + goplsIssues, lintIssues := countIssues(report) + hasIssues := goplsIssues > 0 || lintIssues > 0 + if hasIssues { + // Show TUI block so the user sees it too. var msgLines []string - msgLines = append(msgLines, fmt.Sprintf("File: %s", filepath.Base(absPath))) + msgLines = append(msgLines, fmt.Sprintf("Files: %s", strings.Join(fileNames, ", "))) if goplsIssues > 0 { msgLines = append(msgLines, fmt.Sprintf("gopls: %d issue(s)", goplsIssues)) } if lintIssues > 0 { msgLines = append(msgLines, fmt.Sprintf("golangci-lint: %d issue(s)", lintIssues)) } - msgLines = append(msgLines, "", "⚠️ Please fix these issues before proceeding.") borderColor := "#f9e2af" // yellow if goplsIssues > 0 && lintIssues > 0 { @@ -78,9 +124,16 @@ func Init(api ext.API) { BorderColor: borderColor, Subtitle: "go-edit-lint", }) - } - return &ext.ToolResultResult{Content: &enhanced} + // Inject a follow-up message so the agent fixes the issues. + ctx.SendMessage(report + "\n\n⚠️ DIAGNOSTICS FOUND: Please review and fix the issues above.") + } else { + ctx.PrintBlock(ext.PrintBlockOpts{ + Text: fmt.Sprintf("Files: %s\n✓ All clean", strings.Join(fileNames, ", ")), + BorderColor: "#a6e3a1", + Subtitle: "go-edit-lint", + }) + } }) } @@ -106,18 +159,6 @@ func resolveGoFilePath(inputJSON, cwd string) (string, bool) { return absPath, true } -func runGoDiagnostics(cwd, absPath string) string { - gopls := runGopls(cwd, absPath) - lint := runGolangCILint(cwd, "./...") - - return fmt.Sprintf( - "\n[gopls]\n%s\n\n[golangci-lint]\n%s\n", - filepath.Base(absPath), - formatToolResult(gopls, "No diagnostics."), - formatToolResult(lint, "No lint issues."), - ) -} - func runGopls(cwd, absPath string) lintResult { ctx, cancel := context.WithTimeout(context.Background(), diagnosticsTimeout) defer cancel() @@ -178,7 +219,9 @@ func formatToolResult(res lintResult, emptyFallback string) string { out := strings.TrimSpace(res.Output) if out == "" { if res.Err == nil { - lines = append(lines, emptyFallback) + if emptyFallback != "" { + lines = append(lines, emptyFallback) + } } } else { lines = append(lines, out) @@ -197,17 +240,15 @@ func truncate(s string, max int) string { } func countIssues(report string) (goplsCount, lintCount int) { - // Extract gopls section goplsStart := strings.Index(report, "[gopls]") lintStart := strings.Index(report, "[golangci-lint]") endTag := strings.Index(report, "") if goplsStart != -1 && lintStart != -1 { goplsSection := report[goplsStart:lintStart] - // Count non-empty lines excluding the header and "No diagnostics." message for _, line := range strings.Split(goplsSection, "\n") { line = strings.TrimSpace(line) - if line != "" && line != "[gopls]" && line != "No diagnostics." { + if line != "" && line != "[gopls]" && line != "No diagnostics." && !strings.HasPrefix(line, "#") { goplsCount++ } } @@ -215,7 +256,6 @@ func countIssues(report string) (goplsCount, lintCount int) { if lintStart != -1 && endTag != -1 { lintSection := report[lintStart:endTag] - // Count non-empty lines excluding the header and "No lint issues." message for _, line := range strings.Split(lintSection, "\n") { line = strings.TrimSpace(line) if line != "" && line != "[golangci-lint]" && line != "No lint issues." { diff --git a/skills/kit-extensions/SKILL.md b/skills/kit-extensions/SKILL.md index def178f7..98d20209 100644 --- a/skills/kit-extensions/SKILL.md +++ b/skills/kit-extensions/SKILL.md @@ -91,7 +91,11 @@ api.OnAgentStart(func(e ext.AgentStartEvent, ctx ext.Context) { // Agent finished responding. api.OnAgentEnd(func(e ext.AgentEndEvent, ctx ext.Context) { // e.Response string - // e.StopReason string — "completed", "cancelled", "error" + // e.StopReason string — "error" (on failure), "completed" (when LLM returns + // empty stop reason), or the raw LLM provider value passed through + // (e.g. "stop", "end_turn", "max_tokens", "tool_use"). + // To detect errors, check e.StopReason == "error". + // Do NOT compare against "completed" for success — instead check != "error". }) ```