agileflow 2.90.7 → 2.91.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +6 -6
  3. package/lib/codebase-indexer.js +810 -0
  4. package/lib/validate-names.js +3 -3
  5. package/package.json +4 -1
  6. package/scripts/obtain-context.js +238 -0
  7. package/scripts/precompact-context.sh +13 -1
  8. package/scripts/query-codebase.js +430 -0
  9. package/scripts/tui/blessed/data/watcher.js +175 -0
  10. package/scripts/tui/blessed/index.js +244 -0
  11. package/scripts/tui/blessed/panels/output.js +95 -0
  12. package/scripts/tui/blessed/panels/sessions.js +143 -0
  13. package/scripts/tui/blessed/panels/trace.js +91 -0
  14. package/scripts/tui/blessed/ui/help.js +77 -0
  15. package/scripts/tui/blessed/ui/screen.js +52 -0
  16. package/scripts/tui/blessed/ui/statusbar.js +51 -0
  17. package/scripts/tui/blessed/ui/tabbar.js +99 -0
  18. package/scripts/tui/index.js +38 -30
  19. package/scripts/validators/README.md +143 -0
  20. package/scripts/validators/component-validator.js +212 -0
  21. package/scripts/validators/json-schema-validator.js +179 -0
  22. package/scripts/validators/markdown-validator.js +153 -0
  23. package/scripts/validators/migration-validator.js +117 -0
  24. package/scripts/validators/security-validator.js +276 -0
  25. package/scripts/validators/story-format-validator.js +176 -0
  26. package/scripts/validators/test-result-validator.js +99 -0
  27. package/scripts/validators/workflow-validator.js +240 -0
  28. package/src/core/agents/accessibility.md +6 -0
  29. package/src/core/agents/adr-writer.md +6 -0
  30. package/src/core/agents/analytics.md +6 -0
  31. package/src/core/agents/api.md +6 -0
  32. package/src/core/agents/ci.md +6 -0
  33. package/src/core/agents/codebase-query.md +237 -0
  34. package/src/core/agents/compliance.md +6 -0
  35. package/src/core/agents/configuration-damage-control.md +6 -0
  36. package/src/core/agents/configuration-visual-e2e.md +6 -0
  37. package/src/core/agents/database.md +10 -0
  38. package/src/core/agents/datamigration.md +6 -0
  39. package/src/core/agents/design.md +6 -0
  40. package/src/core/agents/devops.md +6 -0
  41. package/src/core/agents/documentation.md +6 -0
  42. package/src/core/agents/epic-planner.md +6 -0
  43. package/src/core/agents/integrations.md +6 -0
  44. package/src/core/agents/mentor.md +6 -0
  45. package/src/core/agents/mobile.md +6 -0
  46. package/src/core/agents/monitoring.md +6 -0
  47. package/src/core/agents/multi-expert.md +6 -0
  48. package/src/core/agents/performance.md +6 -0
  49. package/src/core/agents/product.md +6 -0
  50. package/src/core/agents/qa.md +6 -0
  51. package/src/core/agents/readme-updater.md +6 -0
  52. package/src/core/agents/refactor.md +6 -0
  53. package/src/core/agents/research.md +6 -0
  54. package/src/core/agents/security.md +6 -0
  55. package/src/core/agents/testing.md +10 -0
  56. package/src/core/agents/ui.md +6 -0
  57. package/src/core/commands/audit.md +401 -0
  58. package/src/core/commands/board.md +1 -0
  59. package/src/core/commands/epic.md +92 -1
  60. package/src/core/commands/help.md +1 -0
  61. package/src/core/commands/metrics.md +1 -0
  62. package/src/core/commands/research/analyze.md +1 -0
  63. package/src/core/commands/research/ask.md +2 -0
  64. package/src/core/commands/research/import.md +1 -0
  65. package/src/core/commands/research/list.md +2 -0
  66. package/src/core/commands/research/synthesize.md +584 -0
  67. package/src/core/commands/research/view.md +2 -0
  68. package/src/core/commands/status.md +126 -1
  69. package/src/core/commands/story/list.md +9 -9
  70. package/src/core/commands/story/view.md +1 -0
  71. package/src/core/experts/codebase-query/expertise.yaml +190 -0
  72. package/src/core/experts/codebase-query/question.md +73 -0
  73. package/src/core/experts/codebase-query/self-improve.md +105 -0
@@ -0,0 +1,276 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Security Validator
4
+ *
5
+ * Validates files for security issues, secrets, and vulnerabilities.
6
+ *
7
+ * Exit codes:
8
+ * 0 = Success
9
+ * 2 = Error (Claude will attempt to fix)
10
+ * 1 = Warning (logged but not blocking)
11
+ *
12
+ * Usage in agent hooks:
13
+ * hooks:
14
+ * PostToolUse:
15
+ * - matcher: "Write"
16
+ * hooks:
17
+ * - type: command
18
+ * command: "node .agileflow/hooks/validators/security-validator.js"
19
+ */
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+
24
+ let input = '';
25
+ process.stdin.on('data', chunk => input += chunk);
26
+ process.stdin.on('end', () => {
27
+ try {
28
+ const context = JSON.parse(input);
29
+ const filePath = context.tool_input?.file_path;
30
+
31
+ if (!filePath) {
32
+ process.exit(0);
33
+ }
34
+
35
+ // Skip if file doesn't exist
36
+ if (!fs.existsSync(filePath)) {
37
+ console.log(`File not found: ${filePath} (skipping validation)`);
38
+ process.exit(0);
39
+ }
40
+
41
+ // Skip binary files
42
+ if (isBinaryFile(filePath)) {
43
+ process.exit(0);
44
+ }
45
+
46
+ const issues = validateSecurity(filePath);
47
+
48
+ if (issues.length > 0) {
49
+ console.error(`Security issues in ${filePath}:`);
50
+ issues.forEach(i => console.error(` - ${i}`));
51
+ process.exit(2); // Claude will fix
52
+ }
53
+
54
+ console.log(`Security validation passed: ${filePath}`);
55
+ process.exit(0);
56
+ } catch (e) {
57
+ console.error(`Validator error: ${e.message}`);
58
+ process.exit(1);
59
+ }
60
+ });
61
+
62
+ function isBinaryFile(filePath) {
63
+ const binaryExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.pdf', '.zip', '.tar', '.gz'];
64
+ const ext = path.extname(filePath).toLowerCase();
65
+ return binaryExtensions.includes(ext);
66
+ }
67
+
68
+ function validateSecurity(filePath) {
69
+ const issues = [];
70
+
71
+ try {
72
+ const content = fs.readFileSync(filePath, 'utf8');
73
+ const ext = path.extname(filePath).toLowerCase();
74
+ const fileName = path.basename(filePath);
75
+
76
+ // Check for secrets and credentials
77
+ issues.push(...checkSecrets(content, fileName));
78
+
79
+ // Check for SQL injection vulnerabilities
80
+ issues.push(...checkSqlInjection(content, ext));
81
+
82
+ // Check for XSS vulnerabilities
83
+ issues.push(...checkXss(content, ext));
84
+
85
+ // Check for command injection
86
+ issues.push(...checkCommandInjection(content, ext));
87
+
88
+ // Check for path traversal
89
+ issues.push(...checkPathTraversal(content, ext));
90
+
91
+ // Check for insecure crypto
92
+ issues.push(...checkInsecureCrypto(content));
93
+
94
+ // Check for insecure randomness
95
+ issues.push(...checkInsecureRandom(content));
96
+
97
+ } catch (e) {
98
+ issues.push(`Read error: ${e.message}`);
99
+ }
100
+
101
+ return issues;
102
+ }
103
+
104
+ function checkSecrets(content, fileName) {
105
+ const issues = [];
106
+
107
+ // Skip .env.example files
108
+ if (fileName === '.env.example' || fileName === '.env.sample') {
109
+ return issues;
110
+ }
111
+
112
+ const secretPatterns = [
113
+ // API Keys
114
+ { pattern: /['"]sk-[a-zA-Z0-9]{20,}['"]/, message: 'Possible OpenAI API key detected' },
115
+ { pattern: /['"]AIza[a-zA-Z0-9_-]{35}['"]/, message: 'Possible Google API key detected' },
116
+ { pattern: /['"]AKIA[A-Z0-9]{16}['"]/, message: 'Possible AWS access key detected' },
117
+ { pattern: /['"]ghp_[a-zA-Z0-9]{36}['"]/, message: 'Possible GitHub personal access token detected' },
118
+ { pattern: /['"]npm_[a-zA-Z0-9]{36}['"]/, message: 'Possible npm token detected' },
119
+
120
+ // Generic patterns
121
+ { pattern: /password\s*[:=]\s*['"][^'"${\s]{8,}['"](?!\s*;?\s*\/\/\s*example)/i, message: 'Possible hardcoded password detected' },
122
+ { pattern: /api[_-]?key\s*[:=]\s*['"][a-zA-Z0-9]{20,}['"]/i, message: 'Possible hardcoded API key detected' },
123
+ { pattern: /secret\s*[:=]\s*['"][a-zA-Z0-9]{20,}['"]/i, message: 'Possible hardcoded secret detected' },
124
+ { pattern: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/, message: 'Private key detected in source code' },
125
+ { pattern: /-----BEGIN\s+CERTIFICATE-----/, message: 'Certificate detected in source code (verify this is intentional)' },
126
+ ];
127
+
128
+ for (const { pattern, message } of secretPatterns) {
129
+ if (pattern.test(content)) {
130
+ issues.push(`${message} - use environment variables or secrets manager`);
131
+ }
132
+ }
133
+
134
+ return issues;
135
+ }
136
+
137
+ function checkSqlInjection(content, ext) {
138
+ const issues = [];
139
+
140
+ // Only check relevant file types
141
+ if (!['.js', '.ts', '.jsx', '.tsx', '.py', '.php', '.java', '.go', '.rb'].includes(ext)) {
142
+ return issues;
143
+ }
144
+
145
+ // Check for string concatenation in SQL
146
+ const sqlInjectionPatterns = [
147
+ { pattern: /['"`]\s*SELECT\s+.*\+\s*\w+/i, message: 'Possible SQL injection: string concatenation in SELECT query' },
148
+ { pattern: /['"`]\s*INSERT\s+.*\+\s*\w+/i, message: 'Possible SQL injection: string concatenation in INSERT query' },
149
+ { pattern: /['"`]\s*UPDATE\s+.*\+\s*\w+/i, message: 'Possible SQL injection: string concatenation in UPDATE query' },
150
+ { pattern: /['"`]\s*DELETE\s+.*\+\s*\w+/i, message: 'Possible SQL injection: string concatenation in DELETE query' },
151
+ { pattern: /\$\{[^}]+\}.*WHERE/i, message: 'Possible SQL injection: template literal in WHERE clause - use parameterized queries' },
152
+ ];
153
+
154
+ for (const { pattern, message } of sqlInjectionPatterns) {
155
+ if (pattern.test(content)) {
156
+ issues.push(message);
157
+ }
158
+ }
159
+
160
+ return issues;
161
+ }
162
+
163
+ function checkXss(content, ext) {
164
+ const issues = [];
165
+
166
+ // Only check relevant file types
167
+ if (!['.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte', '.html'].includes(ext)) {
168
+ return issues;
169
+ }
170
+
171
+ const xssPatterns = [
172
+ { pattern: /innerHTML\s*=\s*[^'"`]/, message: 'Direct innerHTML assignment detected - sanitize content or use textContent' },
173
+ { pattern: /dangerouslySetInnerHTML/, message: 'dangerouslySetInnerHTML used - ensure content is sanitized' },
174
+ { pattern: /document\.write\s*\(/, message: 'document.write() is dangerous - use DOM manipulation instead' },
175
+ { pattern: /v-html\s*=/, message: 'v-html directive detected - ensure content is sanitized' },
176
+ { pattern: /\{@html\s+/, message: 'Svelte @html directive detected - ensure content is sanitized' },
177
+ ];
178
+
179
+ for (const { pattern, message } of xssPatterns) {
180
+ if (pattern.test(content)) {
181
+ // These are warnings, not blocking errors (they're sometimes necessary)
182
+ console.log(`Warning: ${message}`);
183
+ }
184
+ }
185
+
186
+ return issues;
187
+ }
188
+
189
+ function checkCommandInjection(content, ext) {
190
+ const issues = [];
191
+
192
+ // Only check relevant file types
193
+ if (!['.js', '.ts', '.py', '.php', '.rb', '.sh'].includes(ext)) {
194
+ return issues;
195
+ }
196
+
197
+ const cmdInjectionPatterns = [
198
+ // JavaScript/Node
199
+ { pattern: /exec\s*\(\s*[`'"]\s*\$\{/, message: 'Possible command injection: template literal in exec()' },
200
+ { pattern: /exec\s*\(\s*\w+\s*\+/, message: 'Possible command injection: string concatenation in exec()' },
201
+ { pattern: /execSync\s*\(\s*[`'"]\s*\$\{/, message: 'Possible command injection: template literal in execSync()' },
202
+ { pattern: /spawn\s*\(\s*[`'"]\s*\$\{/, message: 'Possible command injection: template literal in spawn()' },
203
+
204
+ // Python
205
+ { pattern: /os\.system\s*\(\s*f['"]/, message: 'Possible command injection: f-string in os.system()' },
206
+ { pattern: /subprocess\.(call|run|Popen)\s*\(\s*f['"]/, message: 'Possible command injection: f-string in subprocess - use list args instead' },
207
+ ];
208
+
209
+ for (const { pattern, message } of cmdInjectionPatterns) {
210
+ if (pattern.test(content)) {
211
+ issues.push(message);
212
+ }
213
+ }
214
+
215
+ return issues;
216
+ }
217
+
218
+ function checkPathTraversal(content, ext) {
219
+ const issues = [];
220
+
221
+ // Only check relevant file types
222
+ if (!['.js', '.ts', '.py', '.php', '.java', '.go', '.rb'].includes(ext)) {
223
+ return issues;
224
+ }
225
+
226
+ const pathTraversalPatterns = [
227
+ { pattern: /path\.join\s*\([^)]*req\.(params|query|body)/, message: 'Possible path traversal: user input in path.join()' },
228
+ { pattern: /readFile(Sync)?\s*\([^)]*req\.(params|query|body)/, message: 'Possible path traversal: user input in file read' },
229
+ { pattern: /open\s*\(\s*f['"].*\{.*\}/, message: 'Possible path traversal: user input in file open (Python)' },
230
+ ];
231
+
232
+ for (const { pattern, message } of pathTraversalPatterns) {
233
+ if (pattern.test(content)) {
234
+ issues.push(message);
235
+ }
236
+ }
237
+
238
+ return issues;
239
+ }
240
+
241
+ function checkInsecureCrypto(content) {
242
+ const issues = [];
243
+
244
+ const insecureCryptoPatterns = [
245
+ { pattern: /createHash\s*\(\s*['"]md5['"]\s*\)/, message: 'MD5 is insecure for cryptographic use - use SHA-256 or better' },
246
+ { pattern: /createHash\s*\(\s*['"]sha1['"]\s*\)/, message: 'SHA-1 is deprecated - use SHA-256 or better' },
247
+ { pattern: /hashlib\.md5\s*\(/, message: 'MD5 is insecure for cryptographic use - use SHA-256 or better' },
248
+ { pattern: /DES|3DES|RC4/, message: 'Insecure encryption algorithm detected - use AES-256-GCM' },
249
+ { pattern: /ECB/, message: 'ECB mode is insecure - use GCM or CBC with proper IV' },
250
+ ];
251
+
252
+ for (const { pattern, message } of insecureCryptoPatterns) {
253
+ if (pattern.test(content)) {
254
+ issues.push(message);
255
+ }
256
+ }
257
+
258
+ return issues;
259
+ }
260
+
261
+ function checkInsecureRandom(content) {
262
+ const issues = [];
263
+
264
+ const insecureRandomPatterns = [
265
+ { pattern: /Math\.random\s*\(\s*\).*(?:token|key|secret|password|auth|session)/i, message: 'Math.random() used for security-sensitive value - use crypto.randomBytes()' },
266
+ { pattern: /random\.random\s*\(\s*\).*(?:token|key|secret|password|auth|session)/i, message: 'random.random() is not cryptographically secure - use secrets module' },
267
+ ];
268
+
269
+ for (const { pattern, message } of insecureRandomPatterns) {
270
+ if (pattern.test(content)) {
271
+ issues.push(message);
272
+ }
273
+ }
274
+
275
+ return issues;
276
+ }
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Story Format Validator
4
+ *
5
+ * Validates story structure in status.json after Write operations.
6
+ * Ensures stories have required fields and valid values.
7
+ *
8
+ * Exit codes:
9
+ * 0 = Success
10
+ * 2 = Error (Claude will attempt to fix)
11
+ * 1 = Warning (logged but not blocking)
12
+ *
13
+ * Usage in agent hooks (e.g., epic-planner.md):
14
+ * hooks:
15
+ * PostToolUse:
16
+ * - matcher: "Write"
17
+ * hooks:
18
+ * - type: command
19
+ * command: "node .agileflow/hooks/validators/story-format-validator.js"
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+
25
+ let input = '';
26
+ process.stdin.on('data', chunk => input += chunk);
27
+ process.stdin.on('end', () => {
28
+ try {
29
+ const context = JSON.parse(input);
30
+ const filePath = context.tool_input?.file_path;
31
+
32
+ // Only validate status.json
33
+ if (!filePath || !filePath.endsWith('status.json')) {
34
+ process.exit(0);
35
+ }
36
+
37
+ // Skip if file doesn't exist
38
+ if (!fs.existsSync(filePath)) {
39
+ console.log(`File not found: ${filePath} (skipping validation)`);
40
+ process.exit(0);
41
+ }
42
+
43
+ const issues = validateStoryFormat(filePath);
44
+
45
+ if (issues.length > 0) {
46
+ console.error(`Resolve these story format issues in ${filePath}:`);
47
+ issues.forEach(i => console.error(` - ${i}`));
48
+ process.exit(2); // Claude will fix
49
+ }
50
+
51
+ console.log(`Story format validation passed: ${filePath}`);
52
+ process.exit(0);
53
+ } catch (e) {
54
+ console.error(`Validator error: ${e.message}`);
55
+ process.exit(1);
56
+ }
57
+ });
58
+
59
+ function validateStoryFormat(filePath) {
60
+ const issues = [];
61
+
62
+ try {
63
+ const content = fs.readFileSync(filePath, 'utf8');
64
+ const data = JSON.parse(content);
65
+
66
+ // Validate stories array
67
+ if (data.stories) {
68
+ if (!Array.isArray(data.stories)) {
69
+ issues.push('stories must be an array');
70
+ return issues;
71
+ }
72
+
73
+ data.stories.forEach((story, index) => {
74
+ const storyIssues = validateSingleStory(story, index);
75
+ issues.push(...storyIssues);
76
+ });
77
+
78
+ // Check for duplicate IDs
79
+ const ids = data.stories.map(s => s.id).filter(Boolean);
80
+ const duplicates = ids.filter((id, i) => ids.indexOf(id) !== i);
81
+ if (duplicates.length > 0) {
82
+ issues.push(`Duplicate story IDs: ${duplicates.join(', ')}`);
83
+ }
84
+ }
85
+
86
+ // Validate epics if present
87
+ if (data.epics) {
88
+ if (!Array.isArray(data.epics)) {
89
+ issues.push('epics must be an array');
90
+ } else {
91
+ data.epics.forEach((epic, index) => {
92
+ if (!epic.id) {
93
+ issues.push(`Epic at index ${index} missing 'id' field`);
94
+ }
95
+ if (!epic.title && !epic.name) {
96
+ issues.push(`Epic ${epic.id || index} missing 'title' or 'name' field`);
97
+ }
98
+ });
99
+ }
100
+ }
101
+
102
+ // Validate current_story reference if present
103
+ if (data.current_story) {
104
+ if (typeof data.current_story !== 'string') {
105
+ issues.push('current_story must be a string (story ID)');
106
+ } else if (data.stories) {
107
+ const storyExists = data.stories.some(s => s.id === data.current_story);
108
+ if (!storyExists) {
109
+ issues.push(`current_story "${data.current_story}" not found in stories array`);
110
+ }
111
+ }
112
+ }
113
+
114
+ } catch (e) {
115
+ if (e instanceof SyntaxError) {
116
+ issues.push(`Invalid JSON: ${e.message}`);
117
+ } else {
118
+ issues.push(`Read error: ${e.message}`);
119
+ }
120
+ }
121
+
122
+ return issues;
123
+ }
124
+
125
+ function validateSingleStory(story, index) {
126
+ const issues = [];
127
+ const storyRef = story.id || `index ${index}`;
128
+
129
+ // Required fields
130
+ if (!story.id) {
131
+ issues.push(`Story at ${storyRef}: missing 'id' field`);
132
+ } else {
133
+ // ID format validation (US-XXXX or EP-XXXX)
134
+ if (!/^(US|EP|TECH|BUG)-\d{4}$/.test(story.id)) {
135
+ issues.push(`Story ${storyRef}: ID should match pattern US-XXXX, EP-XXXX, TECH-XXXX, or BUG-XXXX`);
136
+ }
137
+ }
138
+
139
+ if (!story.title && !story.name) {
140
+ issues.push(`Story ${storyRef}: missing 'title' or 'name' field`);
141
+ }
142
+
143
+ // Status validation
144
+ const validStatuses = ['pending', 'ready', 'in_progress', 'in-progress', 'in_review', 'in-review', 'completed', 'blocked', 'archived'];
145
+ if (story.status && !validStatuses.includes(story.status)) {
146
+ issues.push(`Story ${storyRef}: invalid status "${story.status}". Valid: ${validStatuses.join(', ')}`);
147
+ }
148
+
149
+ // Priority validation
150
+ const validPriorities = ['critical', 'high', 'medium', 'low'];
151
+ if (story.priority && !validPriorities.includes(story.priority)) {
152
+ issues.push(`Story ${storyRef}: invalid priority "${story.priority}". Valid: ${validPriorities.join(', ')}`);
153
+ }
154
+
155
+ // Owner validation (if present)
156
+ if (story.owner) {
157
+ const validOwners = ['AG-UI', 'AG-API', 'AG-CI', 'AG-DB', 'AG-TEST', 'AG-DOC', 'AG-SEC', 'human'];
158
+ if (!validOwners.includes(story.owner)) {
159
+ issues.push(`Story ${storyRef}: unknown owner "${story.owner}". Valid: ${validOwners.join(', ')}`);
160
+ }
161
+ }
162
+
163
+ // Acceptance criteria should be array
164
+ if (story.acceptance_criteria && !Array.isArray(story.acceptance_criteria)) {
165
+ issues.push(`Story ${storyRef}: acceptance_criteria must be an array`);
166
+ }
167
+
168
+ // Epic reference validation
169
+ if (story.epic_id) {
170
+ if (!/^EP-\d{4}$/.test(story.epic_id)) {
171
+ issues.push(`Story ${storyRef}: epic_id should match pattern EP-XXXX`);
172
+ }
173
+ }
174
+
175
+ return issues;
176
+ }
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Test Result Validator
4
+ *
5
+ * Validates test command outputs for the testing agent.
6
+ * Checks for passing tests and coverage thresholds.
7
+ *
8
+ * Exit codes:
9
+ * 0 = Success
10
+ * 2 = Error (Claude will attempt to fix)
11
+ * 1 = Warning (logged but not blocking)
12
+ *
13
+ * Usage in agent hooks (testing.md):
14
+ * hooks:
15
+ * PostToolUse:
16
+ * - matcher: "Bash"
17
+ * hooks:
18
+ * - type: command
19
+ * command: "node .agileflow/hooks/validators/test-result-validator.js"
20
+ */
21
+
22
+ let input = '';
23
+ process.stdin.on('data', chunk => input += chunk);
24
+ process.stdin.on('end', () => {
25
+ try {
26
+ const context = JSON.parse(input);
27
+ const command = context.tool_input?.command || '';
28
+ const result = context.result || '';
29
+
30
+ // Only validate test-related commands
31
+ const testCommands = ['npm test', 'npm run test', 'jest', 'pytest', 'cargo test', 'go test', 'vitest', 'mocha'];
32
+ const isTestCommand = testCommands.some(tc => command.includes(tc));
33
+
34
+ if (!isTestCommand) {
35
+ process.exit(0); // Not a test command, skip
36
+ }
37
+
38
+ const issues = validateTestResult(command, result);
39
+
40
+ if (issues.length > 0) {
41
+ console.error(`Test validation issues (command: ${command}):`);
42
+ issues.forEach(i => console.error(` - ${i}`));
43
+ process.exit(2); // Claude will fix
44
+ }
45
+
46
+ console.log(`Test validation passed for: ${command}`);
47
+ process.exit(0);
48
+ } catch (e) {
49
+ console.error(`Validator error: ${e.message}`);
50
+ process.exit(1);
51
+ }
52
+ });
53
+
54
+ function validateTestResult(command, result) {
55
+ const issues = [];
56
+ const resultLower = result.toLowerCase();
57
+
58
+ // Check for test failures
59
+ if (resultLower.includes('failed') || resultLower.includes('failure')) {
60
+ // Extract failure count if possible
61
+ const failMatch = result.match(/(\d+)\s*(failed|failure)/i);
62
+ if (failMatch) {
63
+ issues.push(`${failMatch[1]} test(s) failed - fix failing tests before continuing`);
64
+ } else {
65
+ issues.push('Tests failed - fix failing tests before continuing');
66
+ }
67
+ }
68
+
69
+ // Check for errors (not test failures, but execution errors)
70
+ if (resultLower.includes('error:') || resultLower.includes('exception')) {
71
+ if (!resultLower.includes('0 errors')) {
72
+ issues.push('Test execution had errors - check test setup');
73
+ }
74
+ }
75
+
76
+ // Check for coverage warnings (if coverage was run)
77
+ if (resultLower.includes('coverage')) {
78
+ // Look for coverage percentage
79
+ const coverageMatch = result.match(/(\d+(?:\.\d+)?)\s*%\s*(?:coverage|statements|branches|functions|lines)/i);
80
+ if (coverageMatch) {
81
+ const coverage = parseFloat(coverageMatch[1]);
82
+ if (coverage < 70) {
83
+ issues.push(`Coverage at ${coverage}% is below 70% threshold - add more tests`);
84
+ }
85
+ }
86
+ }
87
+
88
+ // Check for "no tests" scenarios
89
+ if (resultLower.includes('no tests') || resultLower.includes('no test') || resultLower.includes('0 tests')) {
90
+ issues.push('No tests were run - ensure test files exist and are properly configured');
91
+ }
92
+
93
+ // Check for timeout
94
+ if (resultLower.includes('timeout') || resultLower.includes('timed out')) {
95
+ issues.push('Test timed out - check for infinite loops or slow async operations');
96
+ }
97
+
98
+ return issues;
99
+ }