@codexstar/bug-hunter 3.0.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 (51) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/LICENSE +21 -0
  3. package/README.md +665 -0
  4. package/SKILL.md +624 -0
  5. package/bin/bug-hunter +222 -0
  6. package/evals/evals.json +362 -0
  7. package/modes/_dispatch.md +121 -0
  8. package/modes/extended.md +94 -0
  9. package/modes/fix-loop.md +115 -0
  10. package/modes/fix-pipeline.md +384 -0
  11. package/modes/large-codebase.md +212 -0
  12. package/modes/local-sequential.md +143 -0
  13. package/modes/loop.md +125 -0
  14. package/modes/parallel.md +113 -0
  15. package/modes/scaled.md +76 -0
  16. package/modes/single-file.md +38 -0
  17. package/modes/small.md +86 -0
  18. package/package.json +56 -0
  19. package/prompts/doc-lookup.md +44 -0
  20. package/prompts/examples/hunter-examples.md +131 -0
  21. package/prompts/examples/skeptic-examples.md +87 -0
  22. package/prompts/fixer.md +103 -0
  23. package/prompts/hunter.md +146 -0
  24. package/prompts/recon.md +159 -0
  25. package/prompts/referee.md +122 -0
  26. package/prompts/skeptic.md +143 -0
  27. package/prompts/threat-model.md +122 -0
  28. package/scripts/bug-hunter-state.cjs +537 -0
  29. package/scripts/code-index.cjs +541 -0
  30. package/scripts/context7-api.cjs +133 -0
  31. package/scripts/delta-mode.cjs +219 -0
  32. package/scripts/dep-scan.cjs +343 -0
  33. package/scripts/doc-lookup.cjs +316 -0
  34. package/scripts/fix-lock.cjs +167 -0
  35. package/scripts/init-test-fixture.sh +19 -0
  36. package/scripts/payload-guard.cjs +197 -0
  37. package/scripts/run-bug-hunter.cjs +892 -0
  38. package/scripts/tests/bug-hunter-state.test.cjs +87 -0
  39. package/scripts/tests/code-index.test.cjs +57 -0
  40. package/scripts/tests/delta-mode.test.cjs +47 -0
  41. package/scripts/tests/fix-lock.test.cjs +36 -0
  42. package/scripts/tests/fixtures/flaky-worker.cjs +63 -0
  43. package/scripts/tests/fixtures/low-confidence-worker.cjs +73 -0
  44. package/scripts/tests/fixtures/success-worker.cjs +42 -0
  45. package/scripts/tests/payload-guard.test.cjs +41 -0
  46. package/scripts/tests/run-bug-hunter.test.cjs +403 -0
  47. package/scripts/tests/test-utils.cjs +59 -0
  48. package/scripts/tests/worktree-harvest.test.cjs +297 -0
  49. package/scripts/triage.cjs +528 -0
  50. package/scripts/worktree-harvest.cjs +516 -0
  51. package/templates/subagent-wrapper.md +109 -0
@@ -0,0 +1,219 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ function usage() {
7
+ console.error('Usage:');
8
+ console.error(' delta-mode.cjs select <indexPath> <changedFilesJsonPath> [hops]');
9
+ console.error(' delta-mode.cjs expand <indexPath> <seedFilesJsonPath> <alreadySelectedFilesJsonPath> [hops]');
10
+ }
11
+
12
+ function readJson(filePath) {
13
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
14
+ }
15
+
16
+ function assertArray(value, label) {
17
+ if (!Array.isArray(value)) {
18
+ throw new Error(`${label} must be an array`);
19
+ }
20
+ }
21
+
22
+ function toPositiveInt(value, fallback) {
23
+ const parsed = Number.parseInt(String(value || ''), 10);
24
+ if (!Number.isInteger(parsed) || parsed <= 0) {
25
+ return fallback;
26
+ }
27
+ return parsed;
28
+ }
29
+
30
+ function normalizeFile(filePath) {
31
+ return path.resolve(String(filePath));
32
+ }
33
+
34
+ function buildGraph(index) {
35
+ const graph = {};
36
+ const reverse = {};
37
+ const files = Object.keys(index.files || {});
38
+
39
+ for (const filePath of files) {
40
+ const deps = (index.files[filePath] && index.files[filePath].dependencies) || [];
41
+ graph[filePath] = [...new Set(deps.map((item) => normalizeFile(item)))];
42
+ }
43
+
44
+ for (const filePath of files) {
45
+ reverse[filePath] = [];
46
+ }
47
+ const reverseFromIndex = index.reverseDependencies || {};
48
+ for (const [filePathRaw, dependentsRaw] of Object.entries(reverseFromIndex)) {
49
+ const filePath = normalizeFile(filePathRaw);
50
+ const dependents = Array.isArray(dependentsRaw)
51
+ ? dependentsRaw.map((item) => normalizeFile(item))
52
+ : [];
53
+ reverse[filePath] = [...new Set(dependents)];
54
+ }
55
+
56
+ return { graph, reverse };
57
+ }
58
+
59
+ function expandByHops({ seeds, graph, reverse, hops }) {
60
+ const selected = new Set(seeds);
61
+ let frontier = new Set(seeds);
62
+
63
+ for (let hop = 0; hop < hops; hop += 1) {
64
+ const next = new Set();
65
+ for (const filePath of frontier) {
66
+ const neighbors = [...(graph[filePath] || []), ...(reverse[filePath] || [])];
67
+ for (const neighbor of neighbors) {
68
+ if (selected.has(neighbor)) {
69
+ continue;
70
+ }
71
+ selected.add(neighbor);
72
+ next.add(neighbor);
73
+ }
74
+ }
75
+ if (next.size === 0) {
76
+ break;
77
+ }
78
+ frontier = next;
79
+ }
80
+
81
+ return selected;
82
+ }
83
+
84
+ function criticalOverlay(index, selected) {
85
+ const entries = Object.entries(index.files || {});
86
+ return entries
87
+ .filter(([filePath, meta]) => {
88
+ if (selected.has(filePath)) {
89
+ return false;
90
+ }
91
+ const risk = String((meta && meta.riskHint) || '').toLowerCase();
92
+ const boundaries = (meta && Array.isArray(meta.trustBoundaries)) ? meta.trustBoundaries : [];
93
+ return risk === 'critical' || boundaries.length > 0;
94
+ })
95
+ .map(([filePath]) => filePath)
96
+ .sort();
97
+ }
98
+
99
+ function select(indexPath, changedFilesJsonPath, hopsRaw) {
100
+ const hops = toPositiveInt(hopsRaw, 2);
101
+ const index = readJson(indexPath);
102
+ const changed = readJson(changedFilesJsonPath);
103
+ assertArray(changed, 'changedFilesJson');
104
+
105
+ const filesInIndex = new Set(Object.keys(index.files || {}));
106
+ const normalizedChanged = [...new Set(changed.map((item) => normalizeFile(item)))];
107
+ const seeds = normalizedChanged.filter((filePath) => filesInIndex.has(filePath));
108
+
109
+ const { graph, reverse } = buildGraph(index);
110
+ const selectedSet = expandByHops({
111
+ seeds,
112
+ graph,
113
+ reverse,
114
+ hops
115
+ });
116
+ const selected = [...selectedSet].sort();
117
+ const overlays = criticalOverlay(index, selectedSet);
118
+
119
+ return {
120
+ ok: true,
121
+ hops,
122
+ changedTotal: normalizedChanged.length,
123
+ changedInIndex: seeds.length,
124
+ selected,
125
+ expansionCandidates: overlays,
126
+ metrics: {
127
+ selectedCount: selected.length,
128
+ expansionCandidatesCount: overlays.length
129
+ }
130
+ };
131
+ }
132
+
133
+ function expand(indexPath, seedFilesJsonPath, alreadySelectedFilesJsonPath, hopsRaw) {
134
+ const hops = toPositiveInt(hopsRaw, 1);
135
+ const index = readJson(indexPath);
136
+ const seedFiles = readJson(seedFilesJsonPath);
137
+ const alreadySelectedFiles = readJson(alreadySelectedFilesJsonPath);
138
+ assertArray(seedFiles, 'seedFilesJson');
139
+ assertArray(alreadySelectedFiles, 'alreadySelectedFilesJson');
140
+
141
+ const filesInIndex = new Set(Object.keys(index.files || {}));
142
+ const seeds = [...new Set(seedFiles.map((item) => normalizeFile(item)))]
143
+ .filter((filePath) => filesInIndex.has(filePath));
144
+ const alreadySelected = new Set(alreadySelectedFiles.map((item) => normalizeFile(item)));
145
+ const { graph, reverse } = buildGraph(index);
146
+ const expandedSet = expandByHops({
147
+ seeds,
148
+ graph,
149
+ reverse,
150
+ hops
151
+ });
152
+ const overlays = criticalOverlay(index, alreadySelected);
153
+ const expanded = [...expandedSet]
154
+ .filter((filePath) => !alreadySelected.has(filePath))
155
+ .sort();
156
+ const overlayOnly = overlays.filter((filePath) => !expandedSet.has(filePath));
157
+
158
+ return {
159
+ ok: true,
160
+ hops,
161
+ seedCount: seeds.length,
162
+ expanded,
163
+ overlayOnly,
164
+ metrics: {
165
+ expandedCount: expanded.length,
166
+ overlayOnlyCount: overlayOnly.length
167
+ }
168
+ };
169
+ }
170
+
171
+ function main() {
172
+ const [command, ...args] = process.argv.slice(2);
173
+ if (!command) {
174
+ usage();
175
+ process.exit(1);
176
+ }
177
+
178
+ if (command === 'select') {
179
+ const [indexPath, changedFilesJsonPath, hopsRaw] = args;
180
+ if (!indexPath || !changedFilesJsonPath) {
181
+ usage();
182
+ process.exit(1);
183
+ }
184
+ const result = select(
185
+ path.resolve(indexPath),
186
+ path.resolve(changedFilesJsonPath),
187
+ hopsRaw
188
+ );
189
+ console.log(JSON.stringify(result, null, 2));
190
+ return;
191
+ }
192
+
193
+ if (command === 'expand') {
194
+ const [indexPath, seedFilesJsonPath, alreadySelectedFilesJsonPath, hopsRaw] = args;
195
+ if (!indexPath || !seedFilesJsonPath || !alreadySelectedFilesJsonPath) {
196
+ usage();
197
+ process.exit(1);
198
+ }
199
+ const result = expand(
200
+ path.resolve(indexPath),
201
+ path.resolve(seedFilesJsonPath),
202
+ path.resolve(alreadySelectedFilesJsonPath),
203
+ hopsRaw
204
+ );
205
+ console.log(JSON.stringify(result, null, 2));
206
+ return;
207
+ }
208
+
209
+ usage();
210
+ process.exit(1);
211
+ }
212
+
213
+ try {
214
+ main();
215
+ } catch (error) {
216
+ const message = error instanceof Error ? error.message : String(error);
217
+ console.error(message);
218
+ process.exit(1);
219
+ }
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * dep-scan.cjs — Dependency CVE scanner for Bug Hunter v3
6
+ *
7
+ * Detects package manager, runs audit, filters HIGH/CRITICAL,
8
+ * searches codebase for usage of vulnerable APIs, classifies reachability.
9
+ *
10
+ * Usage: node dep-scan.cjs --target <path> --output <path>
11
+ */
12
+
13
+ const { spawnSync } = require('child_process');
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ function parseArgs(argv) {
18
+ const args = {};
19
+ for (let i = 2; i < argv.length; i += 1) {
20
+ if (argv[i] === '--target' && argv[i + 1]) {
21
+ args.target = argv[i + 1];
22
+ i += 1;
23
+ continue;
24
+ }
25
+
26
+ if (argv[i] === '--output' && argv[i + 1]) {
27
+ args.output = argv[i + 1];
28
+ i += 1;
29
+ }
30
+ }
31
+ return args;
32
+ }
33
+
34
+ function runCommand({ command, cwd, timeout = 90000 }) {
35
+ const result = spawnSync('bash', ['-lc', command], {
36
+ cwd,
37
+ encoding: 'utf8',
38
+ timeout,
39
+ });
40
+
41
+ const stdout = (result.stdout || '').trim();
42
+ const stderr = (result.stderr || '').trim();
43
+ const timeoutHit = result.signal === 'SIGTERM' && Boolean(result.error);
44
+
45
+ return {
46
+ ok: result.status === 0,
47
+ status: result.status,
48
+ signal: result.signal,
49
+ timedOut: timeoutHit,
50
+ stdout,
51
+ stderr,
52
+ errorMessage: result.error ? String(result.error.message || result.error) : '',
53
+ };
54
+ }
55
+
56
+ function detectEcosystems(targetDir) {
57
+ const checks = [
58
+ { lockfile: 'package-lock.json', ecosystem: 'node', manager: 'npm', command: 'npm audit --json' },
59
+ { lockfile: 'pnpm-lock.yaml', ecosystem: 'node', manager: 'pnpm', command: 'pnpm audit --json' },
60
+ { lockfile: 'yarn.lock', ecosystem: 'node', manager: 'yarn', command: 'yarn npm audit --json' },
61
+ { lockfile: 'bun.lockb', ecosystem: 'node', manager: 'bun', command: 'bun audit --json' },
62
+ { lockfile: 'requirements.txt', ecosystem: 'pip', manager: 'pip', command: 'pip-audit --format json' },
63
+ { lockfile: 'Pipfile.lock', ecosystem: 'pip', manager: 'pipenv', command: 'pip-audit --format json' },
64
+ { lockfile: 'go.sum', ecosystem: 'go', manager: 'go', command: 'govulncheck -json ./...' },
65
+ { lockfile: 'Cargo.lock', ecosystem: 'rust', manager: 'cargo', command: 'cargo audit --json' },
66
+ ];
67
+
68
+ return checks.filter((check) => {
69
+ return fs.existsSync(path.join(targetDir, check.lockfile));
70
+ });
71
+ }
72
+
73
+ function parseJsonFromOutput(raw) {
74
+ if (!raw || raw.trim() === '') {
75
+ return null;
76
+ }
77
+
78
+ const trimmed = raw.trim();
79
+
80
+ try {
81
+ return JSON.parse(trimmed);
82
+ } catch {
83
+ // Continue with fallback parsing.
84
+ }
85
+
86
+ const lines = trimmed.split('\n').map((line) => {
87
+ return line.trim();
88
+ }).filter(Boolean);
89
+
90
+ const jsonLine = lines.find((line) => {
91
+ return line.startsWith('{') || line.startsWith('[');
92
+ });
93
+
94
+ if (jsonLine) {
95
+ try {
96
+ return JSON.parse(jsonLine);
97
+ } catch {
98
+ // Continue with brace slicing fallback.
99
+ }
100
+ }
101
+
102
+ const firstBrace = trimmed.indexOf('{');
103
+ const lastBrace = trimmed.lastIndexOf('}');
104
+ if (firstBrace >= 0 && lastBrace > firstBrace) {
105
+ const candidate = trimmed.slice(firstBrace, lastBrace + 1);
106
+ try {
107
+ return JSON.parse(candidate);
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ return null;
114
+ }
115
+
116
+ function extractNodeFindings(data) {
117
+ const vulnerabilityEntries = Object.entries(data.vulnerabilities || {});
118
+ const fromVulnerabilities = vulnerabilityEntries
119
+ .map(([packageName, info]) => {
120
+ const severity = String(info?.severity || '').toUpperCase();
121
+ if (severity !== 'HIGH' && severity !== 'CRITICAL') {
122
+ return null;
123
+ }
124
+
125
+ const viaEntry = Array.isArray(info.via) ? info.via.find((entry) => {
126
+ return typeof entry === 'object';
127
+ }) : null;
128
+
129
+ return {
130
+ package: packageName,
131
+ version: info.range || info.version || 'unknown',
132
+ severity,
133
+ cve: viaEntry?.cve || viaEntry?.url || 'N/A',
134
+ fixed_version: info.fixAvailable?.version || 'unknown',
135
+ title: viaEntry?.title || (typeof info.via?.[0] === 'string' ? info.via[0] : packageName),
136
+ };
137
+ })
138
+ .filter(Boolean);
139
+
140
+ if (fromVulnerabilities.length > 0) {
141
+ return fromVulnerabilities;
142
+ }
143
+
144
+ const advisoryEntries = Object.entries(data.advisories || {});
145
+ return advisoryEntries
146
+ .map(([, advisory]) => {
147
+ const severity = String(advisory?.severity || '').toUpperCase();
148
+ if (severity !== 'HIGH' && severity !== 'CRITICAL') {
149
+ return null;
150
+ }
151
+
152
+ return {
153
+ package: advisory.module_name,
154
+ version: advisory.findings?.[0]?.version || 'unknown',
155
+ severity,
156
+ cve: advisory.cves?.[0] || advisory.url || 'N/A',
157
+ fixed_version: advisory.patched_versions || 'unknown',
158
+ title: advisory.title || advisory.module_name,
159
+ };
160
+ })
161
+ .filter(Boolean);
162
+ }
163
+
164
+ function extractFindingsByEcosystem({ ecosystem, manager, rawOutput }) {
165
+ const data = parseJsonFromOutput(rawOutput);
166
+ if (!data) {
167
+ return {
168
+ findings: [],
169
+ parseError: `Could not parse JSON output for ${ecosystem}/${manager}`,
170
+ };
171
+ }
172
+
173
+ if (ecosystem === 'node') {
174
+ return { findings: extractNodeFindings(data), parseError: null };
175
+ }
176
+
177
+ // TODO: add richer parsers for pip/go/rust outputs.
178
+ return {
179
+ findings: [],
180
+ parseError: null,
181
+ };
182
+ }
183
+
184
+ function searchReachability({ targetDir, packageName }) {
185
+ const escapedPackage = packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
186
+ const importPattern = `(require\\(|from\\s+)['\"]${escapedPackage}`;
187
+ const result = runCommand({
188
+ command: `rg -l "${importPattern}" "${targetDir}" --type-add "src:*.{js,ts,jsx,tsx,py,go,rs}" -t src`,
189
+ cwd: targetDir,
190
+ timeout: 20000,
191
+ });
192
+
193
+ if (!result.ok || !result.stdout) {
194
+ return { reachability: 'NOT_REACHABLE', evidence: 'No imports found in source files' };
195
+ }
196
+
197
+ const files = result.stdout.split('\n').filter(Boolean);
198
+ const nonTestFiles = files.filter((filePath) => {
199
+ return !filePath.includes('.test.') && !filePath.includes('.spec.') && !filePath.includes('__tests__');
200
+ });
201
+
202
+ if (nonTestFiles.length > 0) {
203
+ const suffix = nonTestFiles.length > 3 ? ` (+${nonTestFiles.length - 3} more)` : '';
204
+ return {
205
+ reachability: 'REACHABLE',
206
+ evidence: `Imported in: ${nonTestFiles.slice(0, 3).join(', ')}${suffix}`,
207
+ };
208
+ }
209
+
210
+ return {
211
+ reachability: 'POTENTIALLY_REACHABLE',
212
+ evidence: `Only imported in test files: ${files.slice(0, 2).join(', ')}`,
213
+ };
214
+ }
215
+
216
+ function writeOutput({ outputPath, payload }) {
217
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
218
+ fs.writeFileSync(outputPath, JSON.stringify(payload, null, 2));
219
+ }
220
+
221
+ function main() {
222
+ const args = parseArgs(process.argv);
223
+ const targetDir = path.resolve(args.target || '.');
224
+ const outputPath = path.resolve(args.output || '.bug-hunter/dep-findings.json');
225
+
226
+ const ecosystems = detectEcosystems(targetDir);
227
+
228
+ if (ecosystems.length === 0) {
229
+ const result = {
230
+ scan_date: new Date().toISOString(),
231
+ ecosystems: [],
232
+ lockfiles: [],
233
+ findings: [],
234
+ summary: {
235
+ total: 0,
236
+ reachable: 0,
237
+ potentially_reachable: 0,
238
+ not_reachable: 0,
239
+ },
240
+ scan_errors: [
241
+ {
242
+ manager: 'none',
243
+ lockfile: 'none',
244
+ reason: 'No supported lockfile found (package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lockb, requirements.txt, go.sum, Cargo.lock)',
245
+ },
246
+ ],
247
+ };
248
+
249
+ writeOutput({ outputPath, payload: result });
250
+ console.log('dep-scan: No supported lockfile found.');
251
+ return;
252
+ }
253
+
254
+ const scanErrors = [];
255
+ const allFindings = [];
256
+
257
+ ecosystems.forEach((eco) => {
258
+ console.log(`dep-scan: Running ${eco.command} in ${targetDir}...`);
259
+ const runResult = runCommand({ command: eco.command, cwd: targetDir });
260
+
261
+ const combinedOutput = [runResult.stdout, runResult.stderr].filter(Boolean).join('\n');
262
+
263
+ if (!runResult.ok && combinedOutput.trim() === '') {
264
+ scanErrors.push({
265
+ manager: eco.manager,
266
+ lockfile: eco.lockfile,
267
+ reason: runResult.errorMessage || `Command failed with status ${String(runResult.status)}`,
268
+ });
269
+ return;
270
+ }
271
+
272
+ const { findings, parseError } = extractFindingsByEcosystem({
273
+ ecosystem: eco.ecosystem,
274
+ manager: eco.manager,
275
+ rawOutput: combinedOutput,
276
+ });
277
+
278
+ if (parseError) {
279
+ scanErrors.push({
280
+ manager: eco.manager,
281
+ lockfile: eco.lockfile,
282
+ reason: parseError,
283
+ });
284
+ }
285
+
286
+ const shouldTreatNonZeroAsError = !runResult.ok && (parseError || findings.length === 0);
287
+ if (shouldTreatNonZeroAsError) {
288
+ scanErrors.push({
289
+ manager: eco.manager,
290
+ lockfile: eco.lockfile,
291
+ reason: runResult.stderr || runResult.errorMessage || `Command failed with status ${String(runResult.status)}`,
292
+ });
293
+ }
294
+
295
+ findings.forEach((finding) => {
296
+ const reach = searchReachability({ targetDir, packageName: finding.package });
297
+ allFindings.push({
298
+ id: `DEP-${String(allFindings.length + 1).padStart(3, '0')}`,
299
+ ecosystem: eco.ecosystem,
300
+ manager: eco.manager,
301
+ lockfile: eco.lockfile,
302
+ ...finding,
303
+ ...reach,
304
+ });
305
+ });
306
+ });
307
+
308
+ const summary = {
309
+ total: allFindings.length,
310
+ reachable: allFindings.filter((finding) => {
311
+ return finding.reachability === 'REACHABLE';
312
+ }).length,
313
+ potentially_reachable: allFindings.filter((finding) => {
314
+ return finding.reachability === 'POTENTIALLY_REACHABLE';
315
+ }).length,
316
+ not_reachable: allFindings.filter((finding) => {
317
+ return finding.reachability === 'NOT_REACHABLE';
318
+ }).length,
319
+ };
320
+
321
+ const result = {
322
+ scan_date: new Date().toISOString(),
323
+ ecosystems: [...new Set(ecosystems.map((eco) => eco.ecosystem))],
324
+ lockfiles: ecosystems.map((eco) => eco.lockfile),
325
+ findings: allFindings,
326
+ summary,
327
+ scan_errors: scanErrors,
328
+ };
329
+
330
+ writeOutput({ outputPath, payload: result });
331
+
332
+ console.log(
333
+ `dep-scan: ${summary.total} HIGH/CRITICAL CVEs | ${summary.reachable} reachable, ${summary.potentially_reachable} potentially reachable, ${summary.not_reachable} not reachable`
334
+ );
335
+
336
+ if (scanErrors.length > 0) {
337
+ console.log(`dep-scan: ${scanErrors.length} scan error(s) recorded (see scan_errors in ${outputPath})`);
338
+ }
339
+
340
+ console.log(`dep-scan: Output written to ${outputPath}`);
341
+ }
342
+
343
+ main();