@arvorco/relentless 0.3.1 → 0.4.2

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 (66) hide show
  1. package/.claude/commands/relentless.convert.md +25 -0
  2. package/.claude/skills/analyze/SKILL.md +113 -40
  3. package/.claude/skills/analyze/templates/analysis-report.md +138 -0
  4. package/.claude/skills/checklist/SKILL.md +143 -51
  5. package/.claude/skills/checklist/templates/checklist.md +43 -11
  6. package/.claude/skills/clarify/SKILL.md +70 -11
  7. package/.claude/skills/constitution/SKILL.md +61 -3
  8. package/.claude/skills/constitution/templates/constitution.md +241 -160
  9. package/.claude/skills/constitution/templates/prompt.md +150 -20
  10. package/.claude/skills/convert/SKILL.md +248 -0
  11. package/.claude/skills/implement/SKILL.md +82 -34
  12. package/.claude/skills/plan/SKILL.md +136 -27
  13. package/.claude/skills/plan/templates/plan.md +92 -9
  14. package/.claude/skills/specify/SKILL.md +110 -19
  15. package/.claude/skills/specify/templates/spec.md +40 -5
  16. package/.claude/skills/tasks/SKILL.md +75 -1
  17. package/.claude/skills/tasks/templates/tasks.md +5 -4
  18. package/CHANGELOG.md +63 -1
  19. package/MANUAL.md +40 -0
  20. package/README.md +262 -10
  21. package/bin/relentless.ts +292 -5
  22. package/package.json +2 -2
  23. package/relentless/config.json +46 -2
  24. package/relentless/constitution.md +2 -2
  25. package/relentless/prompt.md +97 -18
  26. package/src/agents/amp.ts +53 -13
  27. package/src/agents/claude.ts +70 -15
  28. package/src/agents/codex.ts +73 -14
  29. package/src/agents/droid.ts +68 -14
  30. package/src/agents/exec.ts +96 -0
  31. package/src/agents/gemini.ts +59 -16
  32. package/src/agents/opencode.ts +188 -9
  33. package/src/cli/fallback-order.ts +210 -0
  34. package/src/cli/index.ts +63 -0
  35. package/src/cli/mode-flag.ts +198 -0
  36. package/src/cli/review-flags.ts +192 -0
  37. package/src/config/loader.ts +16 -1
  38. package/src/config/schema.ts +157 -2
  39. package/src/execution/runner.ts +144 -21
  40. package/src/init/scaffolder.ts +285 -25
  41. package/src/prd/parser.ts +92 -1
  42. package/src/prd/types.ts +136 -0
  43. package/src/review/index.ts +92 -0
  44. package/src/review/prompt.ts +293 -0
  45. package/src/review/runner.ts +337 -0
  46. package/src/review/tasks/docs.ts +529 -0
  47. package/src/review/tasks/index.ts +80 -0
  48. package/src/review/tasks/lint.ts +436 -0
  49. package/src/review/tasks/quality.ts +760 -0
  50. package/src/review/tasks/security.ts +452 -0
  51. package/src/review/tasks/test.ts +456 -0
  52. package/src/review/tasks/typecheck.ts +323 -0
  53. package/src/review/types.ts +139 -0
  54. package/src/routing/cascade.ts +310 -0
  55. package/src/routing/classifier.ts +338 -0
  56. package/src/routing/estimate.ts +270 -0
  57. package/src/routing/fallback.ts +512 -0
  58. package/src/routing/index.ts +124 -0
  59. package/src/routing/registry.ts +501 -0
  60. package/src/routing/report.ts +570 -0
  61. package/src/routing/router.ts +287 -0
  62. package/src/tui/App.tsx +2 -0
  63. package/src/tui/TUIRunner.tsx +103 -8
  64. package/src/tui/components/CurrentStory.tsx +23 -1
  65. package/src/tui/hooks/useTUI.ts +1 -0
  66. package/src/tui/types.ts +9 -0
@@ -0,0 +1,452 @@
1
+ /**
2
+ * Security Micro-Task
3
+ *
4
+ * Scans changed files for OWASP top security issues and generates
5
+ * fix tasks for critical and high severity vulnerabilities.
6
+ *
7
+ * Features:
8
+ * - Retrieves changed files from git diff
9
+ * - Pattern-based scanning for common security vulnerabilities
10
+ * - Detects hardcoded passwords and API keys (critical)
11
+ * - Detects unsafe eval() and innerHTML (high)
12
+ * - Detects command injection and SQL injection risks (high/critical)
13
+ * - Generates fix tasks for critical/high issues only
14
+ * - Test files are reported with severity "info" only
15
+ * - Includes OWASP category classification
16
+ *
17
+ * @module src/review/tasks/security
18
+ */
19
+
20
+ import type { ReviewTaskResult, FixTask } from "../types";
21
+
22
+ /**
23
+ * Types of security vulnerabilities
24
+ */
25
+ export type VulnerabilityType =
26
+ | "hardcoded_password"
27
+ | "hardcoded_api_key"
28
+ | "unsafe_eval"
29
+ | "xss_risk"
30
+ | "command_injection_risk"
31
+ | "sql_injection_risk";
32
+
33
+ /**
34
+ * Severity levels for vulnerabilities
35
+ */
36
+ export type VulnerabilitySeverity = "critical" | "high" | "medium" | "low" | "info";
37
+
38
+ /**
39
+ * OWASP category for vulnerability classification
40
+ */
41
+ export type OwaspCategory =
42
+ | "A01:2021-Broken Access Control"
43
+ | "A02:2021-Cryptographic Failures"
44
+ | "A03:2021-Injection"
45
+ | "A04:2021-Insecure Design"
46
+ | "A05:2021-Security Misconfiguration"
47
+ | "A06:2021-Vulnerable and Outdated Components"
48
+ | "A07:2021-Identification and Authentication Failures"
49
+ | "A08:2021-Software and Data Integrity Failures"
50
+ | "A09:2021-Security Logging and Monitoring Failures"
51
+ | "A10:2021-Server-Side Request Forgery";
52
+
53
+ /**
54
+ * A detected security vulnerability
55
+ */
56
+ export interface Vulnerability {
57
+ /** Type of vulnerability */
58
+ type: VulnerabilityType;
59
+ /** Severity level */
60
+ severity: VulnerabilitySeverity;
61
+ /** File path where found */
62
+ file: string;
63
+ /** Line number (1-based) */
64
+ line: number;
65
+ /** Description of the issue */
66
+ message: string;
67
+ /** OWASP category */
68
+ owaspCategory: OwaspCategory;
69
+ /** The matched pattern/code snippet */
70
+ match?: string;
71
+ }
72
+
73
+ /**
74
+ * Extended result type for security micro-task
75
+ */
76
+ export interface SecurityResult extends ReviewTaskResult {
77
+ /** The command that was executed */
78
+ command: string;
79
+ /** Number of files scanned */
80
+ scannedFiles: number;
81
+ /** Detected vulnerabilities */
82
+ vulnerabilities?: Vulnerability[];
83
+ /** Human-readable summary */
84
+ summary?: string;
85
+ }
86
+
87
+ /**
88
+ * Options for running security scan
89
+ */
90
+ export interface SecurityOptions {
91
+ /** Working directory for the command */
92
+ cwd?: string;
93
+ /** Custom file reader for testing */
94
+ readFile?: (path: string) => Promise<string>;
95
+ }
96
+
97
+ /**
98
+ * Security pattern definition
99
+ */
100
+ interface SecurityPattern {
101
+ type: VulnerabilityType;
102
+ severity: VulnerabilitySeverity;
103
+ pattern: RegExp;
104
+ message: string;
105
+ owaspCategory: OwaspCategory;
106
+ }
107
+
108
+ /**
109
+ * Security patterns to scan for
110
+ */
111
+ const SECURITY_PATTERNS: SecurityPattern[] = [
112
+ // Hardcoded passwords
113
+ {
114
+ type: "hardcoded_password",
115
+ severity: "critical",
116
+ pattern: /\b(password|pwd|passwd|secret)\s*[:=]\s*["'][^"']+["']/gi,
117
+ message: "Hardcoded password detected. Use environment variables instead.",
118
+ owaspCategory: "A07:2021-Identification and Authentication Failures",
119
+ },
120
+ {
121
+ type: "hardcoded_password",
122
+ severity: "critical",
123
+ pattern: /\bPASSWORD\s*[:=]\s*["'][^"']+["']/g,
124
+ message: "Hardcoded PASSWORD constant detected. Use environment variables instead.",
125
+ owaspCategory: "A07:2021-Identification and Authentication Failures",
126
+ },
127
+ // Hardcoded API keys
128
+ {
129
+ type: "hardcoded_api_key",
130
+ severity: "critical",
131
+ pattern: /\b(api[_-]?key|apikey|api[_-]?secret)\s*[:=]\s*["'][^"']+["']/gi,
132
+ message: "Hardcoded API key detected. Use environment variables instead.",
133
+ owaspCategory: "A02:2021-Cryptographic Failures",
134
+ },
135
+ {
136
+ type: "hardcoded_api_key",
137
+ severity: "critical",
138
+ pattern: /["'](sk-[a-zA-Z0-9]{20,}|AIza[a-zA-Z0-9_-]{35}|ghp_[a-zA-Z0-9]{36})["']/g,
139
+ message: "Hardcoded API key pattern detected. Use environment variables instead.",
140
+ owaspCategory: "A02:2021-Cryptographic Failures",
141
+ },
142
+ // Unsafe eval
143
+ {
144
+ type: "unsafe_eval",
145
+ severity: "high",
146
+ pattern: /\beval\s*\([^)]+\)/g,
147
+ message: "Unsafe eval() detected. Avoid using eval with dynamic content.",
148
+ owaspCategory: "A03:2021-Injection",
149
+ },
150
+ {
151
+ type: "unsafe_eval",
152
+ severity: "high",
153
+ pattern: /new\s+Function\s*\([^)]+\)/g,
154
+ message: "Unsafe Function constructor detected. Avoid dynamic code execution.",
155
+ owaspCategory: "A03:2021-Injection",
156
+ },
157
+ // XSS risks
158
+ {
159
+ type: "xss_risk",
160
+ severity: "high",
161
+ pattern: /\.innerHTML\s*=/g,
162
+ message: "Direct innerHTML assignment detected. Use textContent or sanitize input.",
163
+ owaspCategory: "A03:2021-Injection",
164
+ },
165
+ // Command injection
166
+ {
167
+ type: "command_injection_risk",
168
+ severity: "high",
169
+ pattern: /\b(exec|execSync|spawn)\s*\(\s*["'`].*\+/g,
170
+ message: "Command with string concatenation detected. Use parameterized commands.",
171
+ owaspCategory: "A03:2021-Injection",
172
+ },
173
+ {
174
+ type: "command_injection_risk",
175
+ severity: "high",
176
+ pattern: /\b(exec|execSync|spawn)\s*\(\s*`[^`]*\$\{/g,
177
+ message: "Command with template literal interpolation detected. Sanitize input.",
178
+ owaspCategory: "A03:2021-Injection",
179
+ },
180
+ // SQL injection
181
+ {
182
+ type: "sql_injection_risk",
183
+ severity: "critical",
184
+ pattern: /["'`](SELECT|INSERT|UPDATE|DELETE|DROP)\s+.*["'`]\s*\+/gi,
185
+ message: "SQL query with string concatenation detected. Use parameterized queries.",
186
+ owaspCategory: "A03:2021-Injection",
187
+ },
188
+ {
189
+ type: "sql_injection_risk",
190
+ severity: "critical",
191
+ pattern: /\.(query|execute)\s*\(\s*`[^`]*\$\{/g,
192
+ message: "SQL query with template literal interpolation detected. Use parameterized queries.",
193
+ owaspCategory: "A03:2021-Injection",
194
+ },
195
+ ];
196
+
197
+ /**
198
+ * Code file extensions to scan
199
+ */
200
+ const CODE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
201
+
202
+ /**
203
+ * Check if a file is a test file
204
+ */
205
+ function isTestFile(path: string): boolean {
206
+ return (
207
+ path.includes(".test.") ||
208
+ path.includes(".spec.") ||
209
+ path.includes("/tests/") ||
210
+ path.includes("/test/") ||
211
+ path.includes("__tests__")
212
+ );
213
+ }
214
+
215
+ /**
216
+ * Check if a file should be scanned
217
+ */
218
+ function shouldScanFile(path: string): boolean {
219
+ return CODE_EXTENSIONS.some((ext) => path.endsWith(ext));
220
+ }
221
+
222
+ /**
223
+ * Scan a file's content for security vulnerabilities
224
+ *
225
+ * @param content - File content to scan
226
+ * @param filePath - Path to the file (for context)
227
+ * @returns Array of detected vulnerabilities
228
+ */
229
+ export function scanFileForVulnerabilities(
230
+ content: string,
231
+ filePath: string
232
+ ): Vulnerability[] {
233
+ const vulnerabilities: Vulnerability[] = [];
234
+ const lines = content.split("\n");
235
+ const isTest = isTestFile(filePath);
236
+
237
+ for (const pattern of SECURITY_PATTERNS) {
238
+ // Reset lastIndex for global regex patterns
239
+ pattern.pattern.lastIndex = 0;
240
+
241
+ // Search each line for the pattern
242
+ for (let i = 0; i < lines.length; i++) {
243
+ const line = lines[i];
244
+ pattern.pattern.lastIndex = 0;
245
+ const match = pattern.pattern.exec(line);
246
+
247
+ if (match) {
248
+ // Downgrade severity to "info" for test files
249
+ const severity = isTest ? "info" : pattern.severity;
250
+
251
+ vulnerabilities.push({
252
+ type: pattern.type,
253
+ severity,
254
+ file: filePath,
255
+ line: i + 1,
256
+ message: pattern.message,
257
+ owaspCategory: pattern.owaspCategory,
258
+ match: match[0],
259
+ });
260
+ }
261
+ }
262
+ }
263
+
264
+ return vulnerabilities;
265
+ }
266
+
267
+ /**
268
+ * Create a fix task from a vulnerability
269
+ *
270
+ * @param vulnerability - The detected vulnerability
271
+ * @returns A fix task for the review system
272
+ */
273
+ function createFixTask(vulnerability: Vulnerability): FixTask {
274
+ return {
275
+ type: "security_fix",
276
+ file: vulnerability.file,
277
+ line: vulnerability.line,
278
+ description: `Fix ${vulnerability.type.replace(/_/g, " ")} at line ${vulnerability.line}: ${vulnerability.message}`,
279
+ priority: "critical",
280
+ };
281
+ }
282
+
283
+ /**
284
+ * Generate human-readable summary
285
+ *
286
+ * @param vulnerabilities - Detected vulnerabilities
287
+ * @param scannedFiles - Number of files scanned
288
+ * @returns Human-readable summary string
289
+ */
290
+ function generateSummary(
291
+ vulnerabilities: Vulnerability[],
292
+ scannedFiles: number
293
+ ): string {
294
+ if (vulnerabilities.length === 0) {
295
+ return `${scannedFiles} file${scannedFiles !== 1 ? "s" : ""} scanned, no security issues found`;
296
+ }
297
+
298
+ const bySeverity: Record<VulnerabilitySeverity, number> = {
299
+ critical: 0,
300
+ high: 0,
301
+ medium: 0,
302
+ low: 0,
303
+ info: 0,
304
+ };
305
+
306
+ for (const vuln of vulnerabilities) {
307
+ bySeverity[vuln.severity]++;
308
+ }
309
+
310
+ const parts: string[] = [];
311
+ parts.push(`${scannedFiles} file${scannedFiles !== 1 ? "s" : ""} scanned`);
312
+
313
+ const issueParts: string[] = [];
314
+ if (bySeverity.critical > 0) issueParts.push(`${bySeverity.critical} critical`);
315
+ if (bySeverity.high > 0) issueParts.push(`${bySeverity.high} high`);
316
+ if (bySeverity.medium > 0) issueParts.push(`${bySeverity.medium} medium`);
317
+ if (bySeverity.low > 0) issueParts.push(`${bySeverity.low} low`);
318
+ if (bySeverity.info > 0) issueParts.push(`${bySeverity.info} info`);
319
+
320
+ if (issueParts.length > 0) {
321
+ parts.push(issueParts.join(", "));
322
+ }
323
+
324
+ return parts.join(", ");
325
+ }
326
+
327
+ /**
328
+ * Run the security micro-task
329
+ *
330
+ * Retrieves changed files from git diff, scans them for security issues,
331
+ * and generates fix tasks for critical/high vulnerabilities.
332
+ *
333
+ * @param options - Options including working directory and custom file reader
334
+ * @returns SecurityResult with success status, vulnerabilities, and fix tasks
335
+ *
336
+ * @example
337
+ * ```typescript
338
+ * const result = await runSecurity({ cwd: "/path/to/project" });
339
+ * if (!result.success) {
340
+ * console.log(`${result.vulnerabilities?.length} vulnerabilities found`);
341
+ * result.fixTasks.forEach(task => console.log(task.description));
342
+ * }
343
+ * ```
344
+ */
345
+ export async function runSecurity(
346
+ options: SecurityOptions = {}
347
+ ): Promise<SecurityResult> {
348
+ const cwd = options.cwd || process.cwd();
349
+ const command = "git diff --name-only HEAD~1";
350
+ const startTime = Date.now();
351
+
352
+ try {
353
+ // Get list of changed files
354
+ const proc = Bun.spawn(["git", "diff", "--name-only", "HEAD~1"], {
355
+ cwd,
356
+ stdout: "pipe",
357
+ stderr: "pipe",
358
+ });
359
+
360
+ await proc.exited;
361
+ const stdout = await proc.stdout.text();
362
+ // stderr is captured but not used since git diff errors are rare
363
+ await proc.stderr.text();
364
+
365
+ // Parse changed files
366
+ const changedFiles = stdout
367
+ .split("\n")
368
+ .map((f) => f.trim())
369
+ .filter((f) => f.length > 0 && shouldScanFile(f));
370
+
371
+ const duration = Date.now() - startTime;
372
+
373
+ // If no code files changed, return success
374
+ if (changedFiles.length === 0) {
375
+ return {
376
+ taskType: "security",
377
+ success: true,
378
+ errorCount: 0,
379
+ warningCount: 0,
380
+ fixTasks: [],
381
+ duration,
382
+ command,
383
+ scannedFiles: 0,
384
+ vulnerabilities: [],
385
+ summary: "0 files scanned, no code files in diff",
386
+ };
387
+ }
388
+
389
+ // Scan each file for vulnerabilities
390
+ const allVulnerabilities: Vulnerability[] = [];
391
+
392
+ for (const filePath of changedFiles) {
393
+ try {
394
+ let content: string;
395
+ if (options.readFile) {
396
+ content = await options.readFile(filePath);
397
+ } else {
398
+ const file = Bun.file(`${cwd}/${filePath}`);
399
+ content = await file.text();
400
+ }
401
+
402
+ const vulnerabilities = scanFileForVulnerabilities(content, filePath);
403
+ allVulnerabilities.push(...vulnerabilities);
404
+ } catch {
405
+ // Skip files that can't be read
406
+ continue;
407
+ }
408
+ }
409
+
410
+ // Generate fix tasks for critical/high vulnerabilities
411
+ const criticalOrHigh = allVulnerabilities.filter(
412
+ (v) => v.severity === "critical" || v.severity === "high"
413
+ );
414
+ const fixTasks = criticalOrHigh.map(createFixTask);
415
+
416
+ // Count issues by severity for error/warning counts
417
+ const errorCount = criticalOrHigh.length;
418
+ const warningCount = allVulnerabilities.length - criticalOrHigh.length;
419
+
420
+ // Success if no critical/high issues
421
+ const success = errorCount === 0;
422
+
423
+ return {
424
+ taskType: "security",
425
+ success,
426
+ errorCount,
427
+ warningCount,
428
+ fixTasks,
429
+ duration: Date.now() - startTime,
430
+ command,
431
+ scannedFiles: changedFiles.length,
432
+ vulnerabilities: allVulnerabilities,
433
+ summary: generateSummary(allVulnerabilities, changedFiles.length),
434
+ };
435
+ } catch (error) {
436
+ const duration = Date.now() - startTime;
437
+ const errorMessage = error instanceof Error ? error.message : String(error);
438
+
439
+ return {
440
+ taskType: "security",
441
+ success: false,
442
+ errorCount: 1,
443
+ warningCount: 0,
444
+ fixTasks: [],
445
+ duration,
446
+ command,
447
+ scannedFiles: 0,
448
+ vulnerabilities: [],
449
+ error: `Security scan failed: ${errorMessage}`,
450
+ };
451
+ }
452
+ }