agileflow 2.90.6 → 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.
- package/CHANGELOG.md +10 -0
- package/README.md +6 -6
- package/lib/codebase-indexer.js +810 -0
- package/lib/validate-names.js +3 -3
- package/package.json +4 -1
- package/scripts/obtain-context.js +238 -0
- package/scripts/precompact-context.sh +13 -1
- package/scripts/query-codebase.js +430 -0
- package/scripts/tui/blessed/data/watcher.js +175 -0
- package/scripts/tui/blessed/index.js +244 -0
- package/scripts/tui/blessed/panels/output.js +95 -0
- package/scripts/tui/blessed/panels/sessions.js +143 -0
- package/scripts/tui/blessed/panels/trace.js +91 -0
- package/scripts/tui/blessed/ui/help.js +77 -0
- package/scripts/tui/blessed/ui/screen.js +52 -0
- package/scripts/tui/blessed/ui/statusbar.js +51 -0
- package/scripts/tui/blessed/ui/tabbar.js +99 -0
- package/scripts/tui/index.js +38 -32
- package/scripts/tui/simple-tui.js +8 -5
- package/scripts/validators/README.md +143 -0
- package/scripts/validators/component-validator.js +212 -0
- package/scripts/validators/json-schema-validator.js +179 -0
- package/scripts/validators/markdown-validator.js +153 -0
- package/scripts/validators/migration-validator.js +117 -0
- package/scripts/validators/security-validator.js +276 -0
- package/scripts/validators/story-format-validator.js +176 -0
- package/scripts/validators/test-result-validator.js +99 -0
- package/scripts/validators/workflow-validator.js +240 -0
- package/src/core/agents/accessibility.md +6 -0
- package/src/core/agents/adr-writer.md +6 -0
- package/src/core/agents/analytics.md +6 -0
- package/src/core/agents/api.md +6 -0
- package/src/core/agents/ci.md +6 -0
- package/src/core/agents/codebase-query.md +237 -0
- package/src/core/agents/compliance.md +6 -0
- package/src/core/agents/configuration-damage-control.md +6 -0
- package/src/core/agents/configuration-visual-e2e.md +6 -0
- package/src/core/agents/database.md +10 -0
- package/src/core/agents/datamigration.md +6 -0
- package/src/core/agents/design.md +6 -0
- package/src/core/agents/devops.md +6 -0
- package/src/core/agents/documentation.md +6 -0
- package/src/core/agents/epic-planner.md +6 -0
- package/src/core/agents/integrations.md +6 -0
- package/src/core/agents/mentor.md +6 -0
- package/src/core/agents/mobile.md +6 -0
- package/src/core/agents/monitoring.md +6 -0
- package/src/core/agents/multi-expert.md +6 -0
- package/src/core/agents/performance.md +6 -0
- package/src/core/agents/product.md +6 -0
- package/src/core/agents/qa.md +6 -0
- package/src/core/agents/readme-updater.md +6 -0
- package/src/core/agents/refactor.md +6 -0
- package/src/core/agents/research.md +6 -0
- package/src/core/agents/security.md +6 -0
- package/src/core/agents/testing.md +10 -0
- package/src/core/agents/ui.md +6 -0
- package/src/core/commands/audit.md +401 -0
- package/src/core/commands/board.md +1 -0
- package/src/core/commands/epic.md +92 -1
- package/src/core/commands/help.md +1 -0
- package/src/core/commands/metrics.md +1 -0
- package/src/core/commands/research/analyze.md +1 -0
- package/src/core/commands/research/ask.md +2 -0
- package/src/core/commands/research/import.md +1 -0
- package/src/core/commands/research/list.md +2 -0
- package/src/core/commands/research/synthesize.md +584 -0
- package/src/core/commands/research/view.md +2 -0
- package/src/core/commands/status.md +126 -1
- package/src/core/commands/story/list.md +9 -9
- package/src/core/commands/story/view.md +1 -0
- package/src/core/experts/codebase-query/expertise.yaml +190 -0
- package/src/core/experts/codebase-query/question.md +73 -0
- package/src/core/experts/codebase-query/self-improve.md +105 -0
- package/tools/cli/commands/tui.js +40 -271
|
@@ -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
|
+
}
|