brl-subagent 2.3.4

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +257 -0
  3. package/package.json +49 -0
  4. package/presets/code-reviewer.md +37 -0
  5. package/presets/data-analyst.md +33 -0
  6. package/presets/debugger.md +36 -0
  7. package/presets/dev-agent.md +30 -0
  8. package/presets/rapid-prototyper.md +25 -0
  9. package/presets/refactorer.md +34 -0
  10. package/presets/security-auditor.md +39 -0
  11. package/presets/tech-writer.md +33 -0
  12. package/presets/test-engineer.md +34 -0
  13. package/src/concurrency.ts +186 -0
  14. package/src/diff.ts +102 -0
  15. package/src/event-bus.ts +86 -0
  16. package/src/git.ts +187 -0
  17. package/src/history.ts +177 -0
  18. package/src/index.ts +3824 -0
  19. package/src/logging.ts +135 -0
  20. package/src/messaging.ts +164 -0
  21. package/src/metrics.ts +221 -0
  22. package/src/model-availability.ts +73 -0
  23. package/src/notify-completion.ts +207 -0
  24. package/src/params.ts +225 -0
  25. package/src/preflight.ts +150 -0
  26. package/src/presets.ts +425 -0
  27. package/src/prompt.ts +169 -0
  28. package/src/reports.ts +325 -0
  29. package/src/router.ts +133 -0
  30. package/src/runner.ts +877 -0
  31. package/src/sanitize.ts +365 -0
  32. package/src/scheduler.ts +187 -0
  33. package/src/session-manager.ts +1065 -0
  34. package/src/state.ts +590 -0
  35. package/src/templates.ts +469 -0
  36. package/src/transcript-tail.ts +327 -0
  37. package/src/transcript.ts +119 -0
  38. package/src/tui-format.ts +51 -0
  39. package/src/tui.ts +3103 -0
  40. package/src/types.ts +803 -0
  41. package/src/unit-run.ts +269 -0
  42. package/src/update.ts +109 -0
  43. package/src/validate.ts +302 -0
  44. package/templates/analyze-data.md +9 -0
  45. package/templates/code-review.md +10 -0
  46. package/templates/debug-issue.md +9 -0
  47. package/templates/implement-feature.md +9 -0
  48. package/templates/prototype.md +9 -0
  49. package/templates/refactor.md +9 -0
  50. package/templates/security-audit.md +10 -0
  51. package/templates/write-docs.md +9 -0
  52. package/templates/write-tests.md +10 -0
package/src/reports.ts ADDED
@@ -0,0 +1,325 @@
1
+ /**
2
+ * brl-subagent — Compliance Reports (E5)
3
+ *
4
+ * Generates compliance reports showing which subagents touched which files
5
+ * and what secrets may have been accessed.
6
+ */
7
+
8
+ import type {
9
+ SubagentRun,
10
+ FileAccessReport,
11
+ SecretsExposureEntry,
12
+ SecretsExposureReport,
13
+ ComplianceSummary,
14
+ } from "./types";
15
+ import { computeSLAMetrics } from "./metrics";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Constants
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /** Sensitive file patterns that should be flagged in compliance reports */
22
+ const SENSITIVE_FILE_PATTERNS = [
23
+ /\.env\b/,
24
+ /\.env\./,
25
+ /credentials\.json/,
26
+ /auth\.json/,
27
+ /\.pem\b/,
28
+ /\.key\b/,
29
+ /id_rsa/,
30
+ /\.secrets\b/,
31
+ /secrets\.yml/,
32
+ /config\/credentials/,
33
+ /private\//,
34
+ ];
35
+
36
+ /** Sensitive keywords in task descriptions that should be flagged */
37
+ const SENSITIVE_TASK_KEYWORDS = [
38
+ "password",
39
+ "secret",
40
+ "token",
41
+ "api key",
42
+ "credential",
43
+ ];
44
+
45
+ /** Severity mapping for sensitive file patterns */
46
+ const FILE_SEVERITY_MAP: Record<string, "high" | "medium" | "low"> = {
47
+ ".env": "high",
48
+ ".env.*": "high",
49
+ "credentials.json": "high",
50
+ "auth.json": "high",
51
+ ".pem": "high",
52
+ ".key": "high",
53
+ "id_rsa*": "high",
54
+ ".secrets": "high",
55
+ "secrets.yml": "high",
56
+ "config/credentials": "high",
57
+ "private/": "medium",
58
+ };
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Helper functions
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /**
65
+ * Extract file paths from a git diff string.
66
+ * Parses diff headers to find file paths.
67
+ */
68
+ function extractFilesFromGitDiff(gitDiff: string): string[] {
69
+ const files: string[] = [];
70
+ const lines = gitDiff.split("\n");
71
+
72
+ for (const line of lines) {
73
+ // Match diff --git a/path b/path
74
+ const diffMatch = line.match(/^diff --git a\/(.+?) b\//);
75
+ if (diffMatch) {
76
+ files.push(diffMatch[1]);
77
+ continue;
78
+ }
79
+
80
+ // Match --- a/path or +++ b/path
81
+ const fileMatch = line.match(/^(?:---|\+\+\+) [ab]\/(.+?)$/);
82
+ if (fileMatch) {
83
+ files.push(fileMatch[1]);
84
+ }
85
+ }
86
+
87
+ // Deduplicate
88
+ return [...new Set(files)];
89
+ }
90
+
91
+ /**
92
+ * Extract file paths from output summary using heuristics.
93
+ * Looks for patterns like "src/..." or "Modified: file1.ts".
94
+ */
95
+ function extractFilesFromOutputSummary(outputSummary: string): string[] {
96
+ const files: string[] = [];
97
+
98
+ // Pattern 1: "Modified: filename" or "Created: filename" or "Deleted: filename"
99
+ const actionPatterns = outputSummary.match(/(?:Modified|Created|Deleted|Changed|Updated):\s*([^\n,]+)/gi);
100
+ if (actionPatterns) {
101
+ for (const pattern of actionPatterns) {
102
+ const fileMatch = pattern.match(/(?:Modified|Created|Deleted|Changed|Updated):\s*(.+)/i);
103
+ if (fileMatch) {
104
+ files.push(fileMatch[1].trim());
105
+ }
106
+ }
107
+ }
108
+
109
+ // Pattern 2: File paths with common extensions
110
+ const pathPattern = /(?:src|lib|test|tests|spec|specs|config|docs|examples|scripts|utils|helpers|components|services|models|views|controllers|routes|types)\/[\w\-\/]+\.(?:ts|js|tsx|jsx|json|md|yml|yaml)/g;
111
+ const pathMatches = outputSummary.match(pathPattern);
112
+ if (pathMatches) {
113
+ files.push(...pathMatches);
114
+ }
115
+
116
+ // Pattern 3: Files ending with common extensions
117
+ const filePattern = /(?:^|\s)([\w\-\/]+\.(?:ts|js|tsx|jsx|json|md|yml|yaml))(?:\s|$)/g;
118
+ let match;
119
+ while ((match = filePattern.exec(outputSummary)) !== null) {
120
+ if (match[1] && !files.includes(match[1])) {
121
+ files.push(match[1]);
122
+ }
123
+ }
124
+
125
+ // Deduplicate
126
+ return [...new Set(files)];
127
+ }
128
+
129
+ /**
130
+ * Check if a file path matches any sensitive file pattern.
131
+ */
132
+ function isSensitiveFile(filePath: string): { match: boolean; pattern?: string; severity?: "high" | "medium" | "low" } {
133
+ const normalizedPath = filePath.toLowerCase();
134
+
135
+ for (const pattern of SENSITIVE_FILE_PATTERNS) {
136
+ if (pattern.test(normalizedPath)) {
137
+ // Find the severity
138
+ const patternStr = pattern.toString();
139
+ for (const [key, severity] of Object.entries(FILE_SEVERITY_MAP)) {
140
+ if (patternStr.includes(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))) {
141
+ return { match: true, pattern: key, severity };
142
+ }
143
+ }
144
+ // Default to high if no specific severity found
145
+ return { match: true, pattern: patternStr, severity: "high" };
146
+ }
147
+ }
148
+
149
+ return { match: false };
150
+ }
151
+
152
+ /**
153
+ * Check if a task description contains sensitive keywords.
154
+ */
155
+ function hasSensitiveTaskKeywords(task: string): { match: boolean; keyword?: string } {
156
+ const lowerTask = task.toLowerCase();
157
+
158
+ for (const keyword of SENSITIVE_TASK_KEYWORDS) {
159
+ if (lowerTask.includes(keyword)) {
160
+ return { match: true, keyword };
161
+ }
162
+ }
163
+
164
+ return { match: false };
165
+ }
166
+
167
+ // ---------------------------------------------------------------------------
168
+ // Report generators
169
+ // ---------------------------------------------------------------------------
170
+
171
+ /**
172
+ * Generates a report mapping files to subagent runs that touched them.
173
+ *
174
+ * Parses gitDiff from each run's result (if available) to extract file paths.
175
+ * If gitDiff is not available, uses outputSummary to heuristically find file paths.
176
+ *
177
+ * @param runs - Array of SubagentRun records
178
+ * @returns FileAccessReport mapping file path to array of run IDs that touched it
179
+ */
180
+ export function buildFileAccessReport(runs: SubagentRun[]): FileAccessReport {
181
+ const files: Record<string, string[]> = {};
182
+
183
+ for (const run of runs) {
184
+ const runFiles: string[] = [];
185
+
186
+ // Try to extract from gitDiff first (stored in fullOutput for completed runs)
187
+ if (run.fullOutput) {
188
+ // Check if fullOutput contains git diff content
189
+ if (run.fullOutput.includes("diff --git")) {
190
+ runFiles.push(...extractFilesFromGitDiff(run.fullOutput));
191
+ }
192
+ }
193
+
194
+ // Fall back to outputSummary heuristics
195
+ if (runFiles.length === 0 && run.outputSummary) {
196
+ runFiles.push(...extractFilesFromOutputSummary(run.outputSummary));
197
+ }
198
+
199
+ // Add run ID to each file's list
200
+ for (const file of runFiles) {
201
+ if (!files[file]) {
202
+ files[file] = [];
203
+ }
204
+ if (!files[file].includes(run.id)) {
205
+ files[file].push(run.id);
206
+ }
207
+ }
208
+ }
209
+
210
+ return { files };
211
+ }
212
+
213
+ /**
214
+ * Checks if any subagent runs accessed sensitive files.
215
+ *
216
+ * Checks run task descriptions for suspicious patterns and run outputs
217
+ * for sensitive file access.
218
+ *
219
+ * @param runs - Array of SubagentRun records
220
+ * @returns SecretsExposureReport with any findings
221
+ */
222
+ export function buildSecretsExposureReport(runs: SubagentRun[]): SecretsExposureReport {
223
+ const exposures: SecretsExposureEntry[] = [];
224
+
225
+ for (const run of runs) {
226
+ // Check task description for sensitive keywords
227
+ const taskCheck = hasSensitiveTaskKeywords(run.task);
228
+ if (taskCheck.match && taskCheck.keyword) {
229
+ exposures.push({
230
+ file: `task:${taskCheck.keyword}`,
231
+ runId: run.id,
232
+ runLabel: run.label,
233
+ severity: "medium",
234
+ });
235
+ }
236
+
237
+ // Check output for sensitive file access
238
+ const outputToCheck = [
239
+ run.outputSummary,
240
+ run.fullOutput,
241
+ ].filter(Boolean).join("\n");
242
+
243
+ if (outputToCheck) {
244
+ // Check for direct file access mentions
245
+ for (const pattern of SENSITIVE_FILE_PATTERNS) {
246
+ const patternStr = pattern.toString();
247
+ if (pattern.test(outputToCheck)) {
248
+ // Determine severity based on the pattern
249
+ let severity: "high" | "medium" | "low" = "medium";
250
+ for (const [key, sev] of Object.entries(FILE_SEVERITY_MAP)) {
251
+ if (patternStr.includes(key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))) {
252
+ severity = sev;
253
+ break;
254
+ }
255
+ }
256
+
257
+ exposures.push({
258
+ file: patternStr,
259
+ runId: run.id,
260
+ runLabel: run.label,
261
+ severity,
262
+ });
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ return { exposures };
269
+ }
270
+
271
+ /**
272
+ * Combines metrics, file access, and secrets exposure into a single summary.
273
+ *
274
+ * @param runs - Array of SubagentRun records
275
+ * @returns ComplianceSummary with aggregated data
276
+ */
277
+ export function generateComplianceSummary(runs: SubagentRun[]): ComplianceSummary {
278
+ if (runs.length === 0) {
279
+ return {
280
+ totalRuns: 0,
281
+ dateRange: {
282
+ earliest: "",
283
+ latest: "",
284
+ },
285
+ metrics: computeSLAMetrics([]),
286
+ filesAccessed: 0,
287
+ sensitiveFilesTouched: 0,
288
+ highSeverityFindings: 0,
289
+ };
290
+ }
291
+
292
+ // Calculate date range
293
+ const dates = runs
294
+ .map((r) => new Date(r.startedAt).getTime())
295
+ .filter((d) => !isNaN(d));
296
+
297
+ const earliest = dates.length > 0 ? new Date(Math.min(...dates)).toISOString() : "";
298
+ const latest = dates.length > 0 ? new Date(Math.max(...dates)).toISOString() : "";
299
+
300
+ // Compute SLA metrics
301
+ const metrics = computeSLAMetrics(runs);
302
+
303
+ // Get file access report
304
+ const fileReport = buildFileAccessReport(runs);
305
+ const filesAccessed = Object.keys(fileReport.files).length;
306
+
307
+ // Get secrets exposure report
308
+ const secretsReport = buildSecretsExposureReport(runs);
309
+ const sensitiveFilesTouched = secretsReport.exposures.length;
310
+ const highSeverityFindings = secretsReport.exposures.filter(
311
+ (e) => e.severity === "high"
312
+ ).length;
313
+
314
+ return {
315
+ totalRuns: runs.length,
316
+ dateRange: {
317
+ earliest,
318
+ latest,
319
+ },
320
+ metrics,
321
+ filesAccessed,
322
+ sensitiveFilesTouched,
323
+ highSeverityFindings,
324
+ };
325
+ }
package/src/router.ts ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * brl-subagent — Skill-based Routing
3
+ *
4
+ * Auto-classify a task description to the best preset personality
5
+ * using keyword matching.
6
+ */
7
+
8
+ import type { SubagentPreset } from "./types";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Classification rules — ordered by priority
12
+ // ---------------------------------------------------------------------------
13
+
14
+ interface ClassificationRule {
15
+ preset: string;
16
+ keywords: string[];
17
+ }
18
+
19
+ const CLASSIFICATION_RULES: ClassificationRule[] = [
20
+ {
21
+ preset: "security-auditor",
22
+ keywords: [
23
+ "audit", "security", "vulnerability", "owasp", "cve",
24
+ "exploit", "threat", "penetration", "injection", "xss",
25
+ "csrf", "auth bypass",
26
+ ],
27
+ },
28
+ {
29
+ preset: "code-reviewer",
30
+ keywords: [
31
+ "review", "code review", "pr", "pull request", "quality",
32
+ "best practice", "anti-pattern",
33
+ ],
34
+ },
35
+ {
36
+ preset: "test-engineer",
37
+ keywords: [
38
+ "test", "unit test", "integration test", "coverage", "mock",
39
+ "assertion", "spec", "tdd", "bdd",
40
+ ],
41
+ },
42
+ {
43
+ preset: "tech-writer",
44
+ keywords: [
45
+ "document", "readme", "api doc", "manual", "guide",
46
+ "tutorial", "write docs",
47
+ ],
48
+ },
49
+ {
50
+ preset: "debugger",
51
+ keywords: [
52
+ "debug", "bug", "fix", "crash", "error", "trace",
53
+ "diagnose", "root cause", "reproduce",
54
+ ],
55
+ },
56
+ {
57
+ preset: "refactorer",
58
+ keywords: [
59
+ "refactor", "restructure", "clean up", "dry", "extract",
60
+ "simplify", "rename", "reorganize",
61
+ ],
62
+ },
63
+ {
64
+ preset: "data-analyst",
65
+ keywords: [
66
+ "analyze", "data", "metrics", "statistics", "chart",
67
+ "visualize", "report", "query", "aggregate",
68
+ ],
69
+ },
70
+ {
71
+ preset: "rapid-prototyper",
72
+ keywords: [
73
+ "prototype", "quick", "fast", "hack", "poc", "spike",
74
+ "experiment",
75
+ ],
76
+ },
77
+ {
78
+ preset: "dev-agent",
79
+ keywords: [
80
+ "implement", "build", "develop", "code", "write", "create",
81
+ "add", "change", "modify", "edit",
82
+ ],
83
+ },
84
+ ];
85
+
86
+ /**
87
+ * Check if a keyword matches within a task string.
88
+ * Uses word-boundary matching for short keywords (<=3 chars) to avoid
89
+ * false positives (e.g., "pr" matching inside "prototype").
90
+ */
91
+ function keywordMatches(lower: string, keyword: string): boolean {
92
+ if (keyword.length <= 3) {
93
+ const re = new RegExp(`\\b${escapeRegExp(keyword)}\\b`);
94
+ return re.test(lower);
95
+ }
96
+ return lower.includes(keyword);
97
+ }
98
+
99
+ /** Escape special regex characters in a string. */
100
+ function escapeRegExp(s: string): string {
101
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
102
+ }
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Router
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /**
109
+ * Auto-classify a task description to the best preset personality.
110
+ * Uses case-insensitive keyword matching, checked in priority order.
111
+ * Returns the first matching preset name, or undefined if no match.
112
+ */
113
+ export function autoRoutePreset(
114
+ task: string,
115
+ presets: SubagentPreset[],
116
+ ): string | undefined {
117
+ if (!task || task.trim().length === 0) return undefined;
118
+
119
+ const lower = task.toLowerCase();
120
+
121
+ for (const rule of CLASSIFICATION_RULES) {
122
+ // Only match if the preset actually exists in the available presets
123
+ if (!presets.some((p) => p.name === rule.preset)) continue;
124
+
125
+ for (const keyword of rule.keywords) {
126
+ if (keywordMatches(lower, keyword)) {
127
+ return rule.preset;
128
+ }
129
+ }
130
+ }
131
+
132
+ return undefined;
133
+ }