|
7 | 7 | "time" |
8 | 8 |
|
9 | 9 | "github.com/dotcommander/cclint/internal/cue" |
10 | | - "github.com/dotcommander/cclint/internal/discovery" |
11 | | - "github.com/dotcommander/cclint/internal/frontend" |
12 | 10 | "github.com/dotcommander/cclint/internal/scoring" |
13 | 11 | ) |
14 | 12 |
|
@@ -39,147 +37,13 @@ type LintSummary struct { |
39 | 37 | Results []LintResult |
40 | 38 | } |
41 | 39 |
|
42 | | -// LintAgents runs linting on agent files |
| 40 | +// LintAgents runs linting on agent files using the generic linter. |
43 | 41 | func LintAgents(rootPath string, quiet bool, verbose bool, noCycleCheck bool) (*LintSummary, error) { |
44 | | - // Initialize shared context |
45 | 42 | ctx, err := NewLinterContext(rootPath, quiet, verbose, noCycleCheck) |
46 | 43 | if err != nil { |
47 | 44 | return nil, err |
48 | 45 | } |
49 | | - |
50 | | - // Filter agent files |
51 | | - agentFiles := ctx.FilterFilesByType(discovery.FileTypeAgent) |
52 | | - summary := ctx.NewSummary(len(agentFiles)) |
53 | | - |
54 | | - // Process each agent file |
55 | | - for _, file := range agentFiles { |
56 | | - result := LintResult{ |
57 | | - File: file.RelPath, |
58 | | - Type: "agent", |
59 | | - Success: true, |
60 | | - } |
61 | | - |
62 | | - // Parse frontmatter |
63 | | - fm, err := frontend.ParseYAMLFrontmatter(file.Contents) |
64 | | - if err != nil { |
65 | | - result.Errors = append(result.Errors, cue.ValidationError{ |
66 | | - File: file.RelPath, |
67 | | - Message: fmt.Sprintf("Error parsing frontmatter: %v", err), |
68 | | - Severity: "error", |
69 | | - }) |
70 | | - result.Success = false |
71 | | - summary.FailedFiles++ |
72 | | - summary.TotalErrors++ |
73 | | - } else { |
74 | | - // Validate with CUE |
75 | | - if true { // CUE schemas not loaded yet |
76 | | - errors, err := ctx.Validator.ValidateAgent(fm.Data) |
77 | | - if err != nil { |
78 | | - result.Errors = append(result.Errors, cue.ValidationError{ |
79 | | - File: file.RelPath, |
80 | | - Message: fmt.Sprintf("Validation error: %v", err), |
81 | | - Severity: "error", |
82 | | - }) |
83 | | - } |
84 | | - result.Errors = append(result.Errors, errors...) |
85 | | - summary.TotalErrors += len(errors) |
86 | | - } |
87 | | - |
88 | | - // Additional validation rules - separate errors and suggestions |
89 | | - allIssues := validateAgentSpecific(fm.Data, file.RelPath, file.Contents) |
90 | | - for _, issue := range allIssues { |
91 | | - if issue.Severity == "suggestion" { |
92 | | - result.Suggestions = append(result.Suggestions, issue) |
93 | | - summary.TotalSuggestions++ |
94 | | - } else { |
95 | | - result.Errors = append(result.Errors, issue) |
96 | | - summary.TotalErrors++ |
97 | | - } |
98 | | - } |
99 | | - |
100 | | - // Validate allowed-tools field |
101 | | - toolWarnings := ValidateAllowedTools(fm.Data, file.RelPath, file.Contents) |
102 | | - result.Warnings = append(result.Warnings, toolWarnings...) |
103 | | - summary.TotalWarnings += len(toolWarnings) |
104 | | - |
105 | | - // Cross-file validation (missing skills) |
106 | | - crossErrors := ctx.CrossValidator.ValidateAgent(file.RelPath, file.Contents) |
107 | | - result.Errors = append(result.Errors, crossErrors...) |
108 | | - summary.TotalErrors += len(crossErrors) |
109 | | - |
110 | | - // Secrets detection |
111 | | - secretWarnings := detectSecrets(file.Contents, file.RelPath) |
112 | | - result.Warnings = append(result.Warnings, secretWarnings...) |
113 | | - summary.TotalWarnings += len(secretWarnings) |
114 | | - |
115 | | - if len(result.Errors) == 0 { |
116 | | - summary.SuccessfulFiles++ |
117 | | - } else { |
118 | | - result.Success = false |
119 | | - summary.FailedFiles++ |
120 | | - } |
121 | | - |
122 | | - // Score agent quality |
123 | | - scorer := scoring.NewAgentScorer() |
124 | | - score := scorer.Score(file.Contents, fm.Data, fm.Body) |
125 | | - result.Quality = &score |
126 | | - |
127 | | - // Get improvement recommendations |
128 | | - result.Improvements = GetAgentImprovements(file.Contents, fm.Data) |
129 | | - } |
130 | | - |
131 | | - summary.Results = append(summary.Results, result) |
132 | | - ctx.LogProcessed(file.RelPath, len(result.Errors)) |
133 | | - } |
134 | | - |
135 | | - // Detect circular dependencies (unless disabled) |
136 | | - if !ctx.NoCycleCheck { |
137 | | - cycles := ctx.CrossValidator.DetectCycles() |
138 | | - // Track which agents have been reported to avoid duplicates |
139 | | - cyclesReported := make(map[string]bool) |
140 | | - for _, cycle := range cycles { |
141 | | - cycleDesc := FormatCycle(cycle) |
142 | | - // Only report each unique cycle once |
143 | | - if cyclesReported[cycleDesc] { |
144 | | - continue |
145 | | - } |
146 | | - cyclesReported[cycleDesc] = true |
147 | | - |
148 | | - // Add cycle error to all agent files involved in the cycle |
149 | | - agentsInCycle := make(map[string]bool) |
150 | | - for _, node := range cycle.Path { |
151 | | - parts := strings.SplitN(node, ":", 2) |
152 | | - if len(parts) == 2 && parts[0] == "agent" { |
153 | | - agentsInCycle[parts[1]] = true |
154 | | - } |
155 | | - } |
156 | | - |
157 | | - // Report to each agent once |
158 | | - for agentName := range agentsInCycle { |
159 | | - for i, result := range summary.Results { |
160 | | - resultName := crossExtractAgentName(result.File) |
161 | | - if resultName == agentName { |
162 | | - summary.Results[i].Errors = append(summary.Results[i].Errors, cue.ValidationError{ |
163 | | - File: result.File, |
164 | | - Message: fmt.Sprintf("Circular dependency detected: %s", cycleDesc), |
165 | | - Severity: "error", |
166 | | - Source: cue.SourceCClintObserve, |
167 | | - }) |
168 | | - summary.TotalErrors++ |
169 | | - // Mark file as failed |
170 | | - if summary.Results[i].Success { |
171 | | - summary.Results[i].Success = false |
172 | | - summary.SuccessfulFiles-- |
173 | | - summary.FailedFiles++ |
174 | | - } |
175 | | - break |
176 | | - } |
177 | | - } |
178 | | - } |
179 | | - } |
180 | | - } |
181 | | - |
182 | | - return summary, nil |
| 46 | + return lintBatch(ctx, NewAgentLinter()), nil |
183 | 47 | } |
184 | 48 |
|
185 | 49 | // validateAgentSpecific implements agent-specific validation rules |
@@ -300,29 +164,15 @@ func validateAgentBestPractices(filePath string, contents string, data map[strin |
300 | 164 | fmEndLine := GetFrontmatterEndLine(contents) |
301 | 165 |
|
302 | 166 | // XML tag detection in text fields - FROM ANTHROPIC DOCS |
303 | | - xmlTagPattern := regexp.MustCompile(`<[a-zA-Z][^>]*>`) |
304 | 167 | if description, ok := data["description"].(string); ok { |
305 | | - if xmlTagPattern.MatchString(description) { |
306 | | - suggestions = append(suggestions, cue.ValidationError{ |
307 | | - File: filePath, |
308 | | - Message: "Description contains XML-like tags which are not allowed", |
309 | | - Severity: "error", |
310 | | - Source: cue.SourceAnthropicDocs, |
311 | | - Line: FindFrontmatterFieldLine(contents, "description"), |
312 | | - }) |
| 168 | + if xmlErr := DetectXMLTags(description, "Description", filePath, contents); xmlErr != nil { |
| 169 | + suggestions = append(suggestions, *xmlErr) |
313 | 170 | } |
314 | 171 | } |
315 | 172 |
|
316 | | - // Count total lines (±10% tolerance: 200 base + 20 = 220) |
317 | | - lines := strings.Count(contents, "\n") |
318 | | - if lines > 220 { |
319 | | - suggestions = append(suggestions, cue.ValidationError{ |
320 | | - File: filePath, |
321 | | - Message: fmt.Sprintf("Agent is %d lines. Best practice: keep agents under ~220 lines (200±10%%) - move methodology to skills instead.", lines), |
322 | | - Severity: "suggestion", |
323 | | - Source: cue.SourceCClintObserve, |
324 | | - Line: 1, |
325 | | - }) |
| 173 | + // Count total lines (±10% tolerance: 200 base) |
| 174 | + if sizeErr := CheckSizeLimit(contents, 200, 0.10, "agent", filePath); sizeErr != nil { |
| 175 | + suggestions = append(suggestions, *sizeErr) |
326 | 176 | } |
327 | 177 |
|
328 | 178 | // === BLOAT SECTIONS DETECTOR === |
|
0 commit comments