@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,403 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const test = require('node:test');
5
+
6
+ const {
7
+ makeSandbox,
8
+ readJson,
9
+ resolveSkillScript,
10
+ runJson,
11
+ writeJson
12
+ } = require('./test-utils.cjs');
13
+
14
+ test('run-bug-hunter preflight selects available backend by priority', () => {
15
+ const runner = resolveSkillScript('run-bug-hunter.cjs');
16
+ const skillDir = path.resolve(__dirname, '..', '..');
17
+ const result = runJson('node', [
18
+ runner,
19
+ 'preflight',
20
+ '--skill-dir',
21
+ skillDir,
22
+ '--available-backends',
23
+ 'teams,local-sequential'
24
+ ]);
25
+ assert.equal(result.ok, true);
26
+ assert.equal(result.backend.selected, 'teams');
27
+ });
28
+
29
+ test('run-bug-hunter preflight tolerates missing optional code-index helper', () => {
30
+ const sandbox = makeSandbox('run-bug-hunter-preflight-');
31
+ const runner = resolveSkillScript('run-bug-hunter.cjs');
32
+ const optionalSkillDir = path.join(sandbox, 'skill');
33
+ const scriptsDir = path.join(optionalSkillDir, 'scripts');
34
+ fs.mkdirSync(scriptsDir, { recursive: true });
35
+
36
+ for (const fileName of [
37
+ 'run-bug-hunter.cjs',
38
+ 'bug-hunter-state.cjs',
39
+ 'payload-guard.cjs',
40
+ 'fix-lock.cjs',
41
+ 'doc-lookup.cjs',
42
+ 'context7-api.cjs',
43
+ 'delta-mode.cjs'
44
+ ]) {
45
+ fs.copyFileSync(resolveSkillScript(fileName), path.join(scriptsDir, fileName));
46
+ }
47
+
48
+ const result = runJson('node', [
49
+ path.join(scriptsDir, 'run-bug-hunter.cjs'),
50
+ 'preflight',
51
+ '--skill-dir',
52
+ optionalSkillDir
53
+ ]);
54
+ assert.equal(result.ok, true);
55
+ assert.deepEqual(result.missing, []);
56
+ });
57
+
58
+ test('triage promotes low-only source files into the scan order', () => {
59
+ const sandbox = makeSandbox('triage-low-only-');
60
+ const triage = resolveSkillScript('triage.cjs');
61
+ const scriptsDir = path.join(sandbox, 'scripts');
62
+ fs.mkdirSync(scriptsDir, { recursive: true });
63
+ fs.writeFileSync(path.join(scriptsDir, 'a.cjs'), 'module.exports = 1;\n', 'utf8');
64
+ fs.writeFileSync(path.join(scriptsDir, 'b.cjs'), 'module.exports = 2;\n', 'utf8');
65
+
66
+ const result = runJson('node', [triage, 'scan', sandbox]);
67
+ assert.equal(result.totalFiles, 2);
68
+ assert.equal(result.scannableFiles, 2);
69
+ assert.deepEqual(result.scanOrder, ['scripts/a.cjs', 'scripts/b.cjs']);
70
+ });
71
+
72
+ test('run-bug-hunter run executes chunk loop with retry and journal', () => {
73
+ const sandbox = makeSandbox('run-bug-hunter-');
74
+ const runner = resolveSkillScript('run-bug-hunter.cjs');
75
+ const skillDir = path.resolve(__dirname, '..', '..');
76
+ const filesJsonPath = path.join(sandbox, 'files.json');
77
+ const statePath = path.join(sandbox, '.claude', 'bug-hunter-state.json');
78
+ const journalPath = path.join(sandbox, '.claude', 'bug-hunter-run.log');
79
+ const attemptsFile = path.join(sandbox, 'attempts.json');
80
+
81
+ const sourceA = path.join(sandbox, 'src', 'a.ts');
82
+ const sourceB = path.join(sandbox, 'src', 'b.ts');
83
+ fs.mkdirSync(path.dirname(sourceA), { recursive: true });
84
+ fs.writeFileSync(sourceA, 'export const a = 1;\n', 'utf8');
85
+ fs.writeFileSync(sourceB, 'export const b = 2;\n', 'utf8');
86
+ writeJson(filesJsonPath, [sourceA, sourceB]);
87
+
88
+ const flakyWorker = resolveSkillScript('tests', 'fixtures', 'flaky-worker.cjs');
89
+ const workerTemplate = [
90
+ 'node',
91
+ flakyWorker,
92
+ '--chunk-id',
93
+ '{chunkId}',
94
+ '--scan-files-json',
95
+ '{scanFilesJson}',
96
+ '--findings-json',
97
+ '{findingsJson}',
98
+ '--attempts-file',
99
+ attemptsFile
100
+ ].join(' ');
101
+
102
+ const result = runJson('node', [
103
+ runner,
104
+ 'run',
105
+ '--skill-dir',
106
+ skillDir,
107
+ '--files-json',
108
+ filesJsonPath,
109
+ '--state',
110
+ statePath,
111
+ '--mode',
112
+ 'extended',
113
+ '--chunk-size',
114
+ '1',
115
+ '--worker-cmd',
116
+ workerTemplate,
117
+ '--timeout-ms',
118
+ '5000',
119
+ '--max-retries',
120
+ '1',
121
+ '--backoff-ms',
122
+ '10',
123
+ '--journal-path',
124
+ journalPath
125
+ ], {
126
+ cwd: sandbox
127
+ });
128
+
129
+ assert.equal(result.ok, true);
130
+ assert.equal(result.status.chunkStatus.done, 2);
131
+ assert.equal(result.status.metrics.findingsUnique >= 2, true);
132
+
133
+ const attempts = readJson(attemptsFile);
134
+ assert.equal(attempts['chunk-1'], 2);
135
+ assert.equal(attempts['chunk-2'], 2);
136
+
137
+ const journal = fs.readFileSync(journalPath, 'utf8');
138
+ assert.match(journal, /attempt-start/);
139
+ assert.match(journal, /retry-backoff/);
140
+ assert.match(journal, /chunk-done/);
141
+ });
142
+
143
+ test('run-bug-hunter integrates index+delta, fact cards, consistency pass, and fix plan', () => {
144
+ const sandbox = makeSandbox('run-bug-hunter-delta-');
145
+ const runner = resolveSkillScript('run-bug-hunter.cjs');
146
+ const skillDir = path.resolve(__dirname, '..', '..');
147
+ const filesJsonPath = path.join(sandbox, 'files.json');
148
+ const changedFilesJsonPath = path.join(sandbox, 'changed-files.json');
149
+ const statePath = path.join(sandbox, '.claude', 'bug-hunter-state.json');
150
+ const journalPath = path.join(sandbox, '.claude', 'bug-hunter-run.log');
151
+ const seenFilesPath = path.join(sandbox, 'seen-files.json');
152
+ const consistencyReportPath = path.join(sandbox, '.claude', 'bug-hunter-consistency.json');
153
+ const fixPlanPath = path.join(sandbox, '.claude', 'bug-hunter-fix-plan.json');
154
+ const factsPath = path.join(sandbox, '.claude', 'bug-hunter-facts.json');
155
+
156
+ const changedFile = path.join(sandbox, 'src', 'feature', 'changed.ts');
157
+ const depFile = path.join(sandbox, 'src', 'feature', 'dep.ts');
158
+ const overlayFile = path.join(sandbox, 'src', 'api', 'admin-route.ts');
159
+ fs.mkdirSync(path.dirname(changedFile), { recursive: true });
160
+ fs.mkdirSync(path.dirname(overlayFile), { recursive: true });
161
+ fs.writeFileSync(changedFile, "import { dep } from './dep';\nexport const value = dep();\n", 'utf8');
162
+ fs.writeFileSync(depFile, 'export function dep() { return 1; }\n', 'utf8');
163
+ fs.writeFileSync(overlayFile, 'export function handler(req) { return req.body; }\n', 'utf8');
164
+
165
+ writeJson(filesJsonPath, [changedFile, depFile, overlayFile]);
166
+ writeJson(changedFilesJsonPath, [changedFile]);
167
+
168
+ const worker = resolveSkillScript('tests', 'fixtures', 'low-confidence-worker.cjs');
169
+ const workerTemplate = [
170
+ 'node',
171
+ worker,
172
+ '--chunk-id',
173
+ '{chunkId}',
174
+ '--scan-files-json',
175
+ '{scanFilesJson}',
176
+ '--findings-json',
177
+ '{findingsJson}',
178
+ '--facts-json',
179
+ '{factsJson}',
180
+ '--seen-files',
181
+ seenFilesPath,
182
+ '--confidence',
183
+ '60'
184
+ ].join(' ');
185
+
186
+ const result = runJson('node', [
187
+ runner,
188
+ 'run',
189
+ '--skill-dir',
190
+ skillDir,
191
+ '--files-json',
192
+ filesJsonPath,
193
+ '--changed-files-json',
194
+ changedFilesJsonPath,
195
+ '--state',
196
+ statePath,
197
+ '--mode',
198
+ 'extended',
199
+ '--chunk-size',
200
+ '1',
201
+ '--worker-cmd',
202
+ workerTemplate,
203
+ '--timeout-ms',
204
+ '5000',
205
+ '--max-retries',
206
+ '1',
207
+ '--backoff-ms',
208
+ '10',
209
+ '--journal-path',
210
+ journalPath,
211
+ '--consistency-report',
212
+ consistencyReportPath,
213
+ '--fix-plan-path',
214
+ fixPlanPath,
215
+ '--facts-path',
216
+ factsPath,
217
+ '--use-index',
218
+ 'true',
219
+ '--delta-mode',
220
+ 'true',
221
+ '--delta-hops',
222
+ '1',
223
+ '--expand-on-low-confidence',
224
+ 'true',
225
+ '--confidence-threshold',
226
+ '75',
227
+ '--canary-size',
228
+ '1'
229
+ ], {
230
+ cwd: sandbox
231
+ });
232
+
233
+ assert.equal(result.ok, true);
234
+ assert.equal(result.deltaMode, true);
235
+ assert.equal(result.deltaSummary.selectedCount >= 2, true);
236
+ assert.equal(fs.existsSync(consistencyReportPath), true);
237
+ assert.equal(fs.existsSync(fixPlanPath), true);
238
+ assert.equal(fs.existsSync(factsPath), true);
239
+
240
+ const seenFiles = readJson(seenFilesPath);
241
+ assert.equal(seenFiles.includes(overlayFile), true);
242
+
243
+ const state = readJson(statePath);
244
+ assert.equal(Object.keys(state.factCards || {}).length >= 3, true);
245
+ assert.equal(state.metrics.lowConfidenceFindings >= 1, true);
246
+
247
+ const consistency = readJson(consistencyReportPath);
248
+ assert.equal(consistency.lowConfidenceFindings >= 1, true);
249
+
250
+ const fixPlan = readJson(fixPlanPath);
251
+ assert.equal(fixPlan.totals.manualReview >= 1, true);
252
+ });
253
+
254
+ test('run-bug-hunter builds canary fix subset from high-confidence findings', () => {
255
+ const sandbox = makeSandbox('run-bug-hunter-canary-');
256
+ const runner = resolveSkillScript('run-bug-hunter.cjs');
257
+ const skillDir = path.resolve(__dirname, '..', '..');
258
+ const filesJsonPath = path.join(sandbox, 'files.json');
259
+ const statePath = path.join(sandbox, '.claude', 'bug-hunter-state.json');
260
+ const fixPlanPath = path.join(sandbox, '.claude', 'bug-hunter-fix-plan.json');
261
+
262
+ const fileA = path.join(sandbox, 'src', 'a.ts');
263
+ const fileB = path.join(sandbox, 'src', 'b.ts');
264
+ fs.mkdirSync(path.dirname(fileA), { recursive: true });
265
+ fs.writeFileSync(fileA, 'export const a = 1;\n', 'utf8');
266
+ fs.writeFileSync(fileB, 'export const b = 2;\n', 'utf8');
267
+ writeJson(filesJsonPath, [fileA, fileB]);
268
+
269
+ const worker = resolveSkillScript('tests', 'fixtures', 'low-confidence-worker.cjs');
270
+ const workerTemplate = [
271
+ 'node',
272
+ worker,
273
+ '--chunk-id',
274
+ '{chunkId}',
275
+ '--scan-files-json',
276
+ '{scanFilesJson}',
277
+ '--findings-json',
278
+ '{findingsJson}',
279
+ '--facts-json',
280
+ '{factsJson}',
281
+ '--confidence',
282
+ '92'
283
+ ].join(' ');
284
+
285
+ runJson('node', [
286
+ runner,
287
+ 'run',
288
+ '--skill-dir',
289
+ skillDir,
290
+ '--files-json',
291
+ filesJsonPath,
292
+ '--state',
293
+ statePath,
294
+ '--mode',
295
+ 'extended',
296
+ '--chunk-size',
297
+ '1',
298
+ '--worker-cmd',
299
+ workerTemplate,
300
+ '--timeout-ms',
301
+ '5000',
302
+ '--confidence-threshold',
303
+ '75',
304
+ '--fix-plan-path',
305
+ fixPlanPath,
306
+ '--canary-size',
307
+ '1'
308
+ ], {
309
+ cwd: sandbox
310
+ });
311
+
312
+ const fixPlan = readJson(fixPlanPath);
313
+ assert.equal(fixPlan.totals.eligible >= 1, true);
314
+ assert.equal(fixPlan.totals.canary, 1);
315
+ });
316
+
317
+ test('run-bug-hunter respects configured delta hops during low-confidence expansion', () => {
318
+ const sandbox = makeSandbox('run-bug-hunter-delta-hops-');
319
+ const runner = resolveSkillScript('run-bug-hunter.cjs');
320
+ const skillDir = path.resolve(__dirname, '..', '..');
321
+ const filesJsonPath = path.join(sandbox, 'files.json');
322
+ const changedFilesJsonPath = path.join(sandbox, 'changed-files.json');
323
+ const statePath = path.join(sandbox, '.claude', 'bug-hunter-state.json');
324
+ const seenFilesPath = path.join(sandbox, 'seen-files.json');
325
+ const workerPath = path.join(sandbox, 'worker.cjs');
326
+ const changedFile = path.join(sandbox, 'src', 'a.ts');
327
+ const neighborFile = path.join(sandbox, 'src', 'b.ts');
328
+ const twoHopFile = path.join(sandbox, 'src', 'c.ts');
329
+
330
+ fs.mkdirSync(path.dirname(changedFile), { recursive: true });
331
+ fs.writeFileSync(changedFile, "import { b } from './b';\nexport const a = b();\n", 'utf8');
332
+ fs.writeFileSync(neighborFile, "import { c } from './c';\nexport function b() { return c(); }\n", 'utf8');
333
+ fs.writeFileSync(twoHopFile, 'export function c() { return 1; }\n', 'utf8');
334
+
335
+ writeJson(filesJsonPath, [changedFile, neighborFile, twoHopFile]);
336
+ writeJson(changedFilesJsonPath, [changedFile]);
337
+
338
+ fs.writeFileSync(workerPath, [
339
+ '#!/usr/bin/env node',
340
+ "const fs = require('fs');",
341
+ "const seenPath = process.argv[process.argv.indexOf('--seen-files') + 1];",
342
+ "const changedPath = process.argv[process.argv.indexOf('--changed-file') + 1];",
343
+ "const scanPath = process.argv[process.argv.indexOf('--scan-files-json') + 1];",
344
+ "const findingsPath = process.argv[process.argv.indexOf('--findings-json') + 1];",
345
+ "const scan = JSON.parse(fs.readFileSync(scanPath, 'utf8'));",
346
+ 'let seen = [];',
347
+ "if (fs.existsSync(seenPath)) seen = JSON.parse(fs.readFileSync(seenPath, 'utf8'));",
348
+ 'seen.push(scan);',
349
+ "fs.writeFileSync(seenPath, JSON.stringify(seen));",
350
+ "const findings = scan[0] === changedPath ? [{ file: scan[0], lines: '1', claim: 'low confidence', severity: 'Low', confidence: 60 }] : [];",
351
+ "fs.writeFileSync(findingsPath, JSON.stringify(findings));"
352
+ ].join('\n'), 'utf8');
353
+
354
+ const workerTemplate = [
355
+ 'node',
356
+ workerPath,
357
+ '--chunk-id',
358
+ '{chunkId}',
359
+ '--scan-files-json',
360
+ '{scanFilesJson}',
361
+ '--findings-json',
362
+ '{findingsJson}',
363
+ '--seen-files',
364
+ seenFilesPath,
365
+ '--changed-file',
366
+ changedFile
367
+ ].join(' ');
368
+
369
+ const result = runJson('node', [
370
+ runner,
371
+ 'run',
372
+ '--skill-dir',
373
+ skillDir,
374
+ '--files-json',
375
+ filesJsonPath,
376
+ '--changed-files-json',
377
+ changedFilesJsonPath,
378
+ '--state',
379
+ statePath,
380
+ '--mode',
381
+ 'extended',
382
+ '--chunk-size',
383
+ '1',
384
+ '--worker-cmd',
385
+ workerTemplate,
386
+ '--use-index',
387
+ 'true',
388
+ '--delta-mode',
389
+ 'true',
390
+ '--delta-hops',
391
+ '1',
392
+ '--expand-on-low-confidence',
393
+ 'true',
394
+ '--confidence-threshold',
395
+ '75'
396
+ ], {
397
+ cwd: sandbox
398
+ });
399
+
400
+ assert.equal(result.ok, true);
401
+ const seenFiles = readJson(seenFilesPath).flat();
402
+ assert.equal(seenFiles.includes(twoHopFile), false);
403
+ });
@@ -0,0 +1,59 @@
1
+ const childProcess = require('child_process');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const path = require('path');
5
+
6
+ function runJson(cmd, args, options = {}) {
7
+ const result = childProcess.spawnSync(cmd, args, {
8
+ encoding: 'utf8',
9
+ ...options
10
+ });
11
+ if (result.status !== 0) {
12
+ const stderr = (result.stderr || '').trim();
13
+ const stdout = (result.stdout || '').trim();
14
+ const message = stderr || stdout || `${cmd} failed`;
15
+ const error = new Error(message);
16
+ error.result = result;
17
+ throw error;
18
+ }
19
+ const output = (result.stdout || '').trim();
20
+ if (!output) {
21
+ return {};
22
+ }
23
+ return JSON.parse(output);
24
+ }
25
+
26
+ function runRaw(cmd, args, options = {}) {
27
+ return childProcess.spawnSync(cmd, args, {
28
+ encoding: 'utf8',
29
+ ...options
30
+ });
31
+ }
32
+
33
+ function makeSandbox(prefix = 'bug-hunter-test-') {
34
+ const tmpBase = path.resolve('tmp');
35
+ fs.mkdirSync(tmpBase, { recursive: true });
36
+ return fs.mkdtempSync(path.join(tmpBase, prefix));
37
+ }
38
+
39
+ function writeJson(filePath, value) {
40
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
41
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
42
+ }
43
+
44
+ function readJson(filePath) {
45
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
46
+ }
47
+
48
+ function resolveSkillScript(...parts) {
49
+ return path.resolve(__dirname, '..', ...parts);
50
+ }
51
+
52
+ module.exports = {
53
+ readJson,
54
+ resolveSkillScript,
55
+ runJson,
56
+ runRaw,
57
+ makeSandbox,
58
+ writeJson
59
+ };