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".
})
```