@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,537 @@
1
+ #!/usr/bin/env node
2
+
3
+ const crypto = require('crypto');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const VALID_CHUNK_STATUS = new Set(['pending', 'in_progress', 'done', 'failed']);
8
+ const DEFAULT_CHUNK_SIZE = 30;
9
+
10
+ function nowIso() {
11
+ return new Date().toISOString();
12
+ }
13
+
14
+ function ensureDir(filePath) {
15
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
16
+ }
17
+
18
+ function readJson(filePath) {
19
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
20
+ }
21
+
22
+ function writeJson(filePath, value) {
23
+ ensureDir(filePath);
24
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
25
+ }
26
+
27
+ function splitChunks(files, chunkSize) {
28
+ const chunks = [];
29
+ let index = 0;
30
+ while (index < files.length) {
31
+ const filesSlice = files.slice(index, index + chunkSize);
32
+ chunks.push({
33
+ id: `chunk-${chunks.length + 1}`,
34
+ files: filesSlice,
35
+ status: 'pending',
36
+ retries: 0,
37
+ startedAt: null,
38
+ completedAt: null,
39
+ lastError: null
40
+ });
41
+ index += chunkSize;
42
+ }
43
+ return chunks;
44
+ }
45
+
46
+ function nextChunkNumber(chunks) {
47
+ const maxId = chunks.reduce((max, chunk) => {
48
+ const match = /^chunk-(\d+)$/.exec(String(chunk.id || ''));
49
+ if (!match) {
50
+ return max;
51
+ }
52
+ const value = Number.parseInt(match[1], 10);
53
+ if (!Number.isInteger(value)) {
54
+ return max;
55
+ }
56
+ return Math.max(max, value);
57
+ }, 0);
58
+ return maxId + 1;
59
+ }
60
+
61
+ function buildInitialState({ mode, chunkSize, files }) {
62
+ const normalizedFiles = [...new Set(files)].sort();
63
+ return {
64
+ schemaVersion: 2,
65
+ mode,
66
+ createdAt: nowIso(),
67
+ updatedAt: nowIso(),
68
+ chunkSize,
69
+ runtime: {
70
+ parallelDisabled: false
71
+ },
72
+ metrics: {
73
+ filesTotal: normalizedFiles.length,
74
+ filesScanned: 0,
75
+ chunksTotal: Math.ceil(normalizedFiles.length / chunkSize),
76
+ chunksDone: 0,
77
+ findingsTotal: 0,
78
+ findingsUnique: 0,
79
+ lowConfidenceFindings: 0
80
+ },
81
+ chunks: splitChunks(normalizedFiles, chunkSize),
82
+ bugLedger: [],
83
+ hashCache: {},
84
+ factCards: {},
85
+ consistency: {
86
+ checkedAt: null,
87
+ conflicts: []
88
+ },
89
+ fixPlan: null
90
+ };
91
+ }
92
+
93
+ function readState(statePath) {
94
+ if (!fs.existsSync(statePath)) {
95
+ throw new Error(`State file does not exist: ${statePath}`);
96
+ }
97
+ return readJson(statePath);
98
+ }
99
+
100
+ function saveState(statePath, state) {
101
+ state.updatedAt = nowIso();
102
+ writeJson(statePath, state);
103
+ }
104
+
105
+ function hashFile(filePath) {
106
+ const data = fs.readFileSync(filePath);
107
+ return crypto.createHash('sha256').update(data).digest('hex');
108
+ }
109
+
110
+ function summarize(state) {
111
+ const pending = state.chunks.filter((chunk) => chunk.status === 'pending').length;
112
+ const inProgress = state.chunks.filter((chunk) => chunk.status === 'in_progress').length;
113
+ const done = state.chunks.filter((chunk) => chunk.status === 'done').length;
114
+ const failed = state.chunks.filter((chunk) => chunk.status === 'failed').length;
115
+ return {
116
+ schemaVersion: state.schemaVersion,
117
+ mode: state.mode,
118
+ updatedAt: state.updatedAt,
119
+ runtime: state.runtime,
120
+ metrics: state.metrics,
121
+ chunkStatus: {
122
+ pending,
123
+ inProgress,
124
+ done,
125
+ failed
126
+ }
127
+ };
128
+ }
129
+
130
+ function severityRank(severity) {
131
+ const normalized = String(severity || '').toLowerCase();
132
+ if (normalized === 'critical') {
133
+ return 3;
134
+ }
135
+ if (normalized === 'medium') {
136
+ return 2;
137
+ }
138
+ if (normalized === 'low') {
139
+ return 1;
140
+ }
141
+ return 0;
142
+ }
143
+
144
+ function usage() {
145
+ console.error('Usage:');
146
+ console.error(' bug-hunter-state.cjs init <statePath> <mode> <filesJsonPath> [chunkSize]');
147
+ console.error(' bug-hunter-state.cjs status <statePath>');
148
+ console.error(' bug-hunter-state.cjs next-chunk <statePath>');
149
+ console.error(' bug-hunter-state.cjs mark-chunk <statePath> <chunkId> <pending|in_progress|done|failed> [error]');
150
+ console.error(' bug-hunter-state.cjs record-findings <statePath> <findingsJsonPath> [source]');
151
+ console.error(' bug-hunter-state.cjs hash-filter <statePath> <filesJsonPath>');
152
+ console.error(' bug-hunter-state.cjs hash-update <statePath> <filesJsonPath> [status]');
153
+ console.error(' bug-hunter-state.cjs append-files <statePath> <filesJsonPath>');
154
+ console.error(' bug-hunter-state.cjs record-fact-card <statePath> <chunkId> <factCardJsonPath>');
155
+ console.error(' bug-hunter-state.cjs set-consistency <statePath> <consistencyJsonPath>');
156
+ console.error(' bug-hunter-state.cjs set-fix-plan <statePath> <fixPlanJsonPath>');
157
+ console.error(' bug-hunter-state.cjs set-parallel-disabled <statePath> <true|false>');
158
+ }
159
+
160
+ function assertArray(value, label) {
161
+ if (!Array.isArray(value)) {
162
+ throw new Error(`${label} must be an array`);
163
+ }
164
+ }
165
+
166
+ function toConfidence(value) {
167
+ if (value === null || value === undefined || value === '') {
168
+ return null;
169
+ }
170
+ const parsed = Number(value);
171
+ if (!Number.isFinite(parsed)) {
172
+ return null;
173
+ }
174
+ return parsed;
175
+ }
176
+
177
+ function main() {
178
+ const [command, ...args] = process.argv.slice(2);
179
+
180
+ if (!command) {
181
+ usage();
182
+ process.exit(1);
183
+ }
184
+
185
+ if (command === 'init') {
186
+ const [statePath, mode, filesJsonPath, chunkSizeRaw] = args;
187
+ if (!statePath || !mode || !filesJsonPath) {
188
+ usage();
189
+ process.exit(1);
190
+ }
191
+ const files = readJson(filesJsonPath);
192
+ assertArray(files, 'filesJson');
193
+ const chunkSizeParsed = Number.parseInt(chunkSizeRaw || '', 10);
194
+ const chunkSize = Number.isInteger(chunkSizeParsed) && chunkSizeParsed > 0
195
+ ? chunkSizeParsed
196
+ : DEFAULT_CHUNK_SIZE;
197
+ const state = buildInitialState({ mode, chunkSize, files });
198
+ saveState(statePath, state);
199
+ console.log(JSON.stringify({
200
+ ok: true,
201
+ statePath,
202
+ summary: summarize(state)
203
+ }, null, 2));
204
+ return;
205
+ }
206
+
207
+ if (command === 'status') {
208
+ const [statePath] = args;
209
+ const state = readState(statePath);
210
+ console.log(JSON.stringify({
211
+ ok: true,
212
+ statePath,
213
+ summary: summarize(state)
214
+ }, null, 2));
215
+ return;
216
+ }
217
+
218
+ if (command === 'next-chunk') {
219
+ const [statePath] = args;
220
+ const state = readState(statePath);
221
+ const nextChunk = state.chunks.find((chunk) => chunk.status === 'pending');
222
+ if (!nextChunk) {
223
+ console.log(JSON.stringify({ ok: true, done: true }, null, 2));
224
+ return;
225
+ }
226
+ console.log(JSON.stringify({ ok: true, done: false, chunk: nextChunk }, null, 2));
227
+ return;
228
+ }
229
+
230
+ if (command === 'mark-chunk') {
231
+ const [statePath, chunkId, status, errorMessage] = args;
232
+ if (!statePath || !chunkId || !status) {
233
+ usage();
234
+ process.exit(1);
235
+ }
236
+ if (!VALID_CHUNK_STATUS.has(status)) {
237
+ throw new Error(`Invalid chunk status: ${status}`);
238
+ }
239
+ const state = readState(statePath);
240
+ const chunk = state.chunks.find((entry) => entry.id === chunkId);
241
+ if (!chunk) {
242
+ throw new Error(`Unknown chunk id: ${chunkId}`);
243
+ }
244
+ chunk.status = status;
245
+ if (status === 'in_progress') {
246
+ chunk.startedAt = nowIso();
247
+ chunk.retries += 1;
248
+ chunk.lastError = null;
249
+ } else if (status === 'done') {
250
+ chunk.completedAt = nowIso();
251
+ chunk.lastError = null;
252
+ } else if (status === 'failed') {
253
+ chunk.lastError = errorMessage || 'unknown';
254
+ }
255
+ state.metrics.chunksDone = state.chunks.filter((entry) => entry.status === 'done').length;
256
+ state.metrics.filesScanned = state.chunks
257
+ .filter((entry) => entry.status === 'done')
258
+ .flatMap((entry) => entry.files)
259
+ .length;
260
+ saveState(statePath, state);
261
+ console.log(JSON.stringify({ ok: true, chunk }, null, 2));
262
+ return;
263
+ }
264
+
265
+ if (command === 'record-findings') {
266
+ const [statePath, findingsJsonPath, source = 'unknown'] = args;
267
+ if (!statePath || !findingsJsonPath) {
268
+ usage();
269
+ process.exit(1);
270
+ }
271
+ const state = readState(statePath);
272
+ const findings = readJson(findingsJsonPath);
273
+ assertArray(findings, 'findingsJson');
274
+
275
+ let inserted = 0;
276
+ let updated = 0;
277
+ for (const finding of findings) {
278
+ const file = String(finding.file || '').trim();
279
+ if (!file) {
280
+ continue;
281
+ }
282
+ const lines = String(finding.lines || '').trim();
283
+ const claim = String(finding.claim || '').trim();
284
+ const severity = String(finding.severity || 'Low');
285
+ const confidence = toConfidence(finding.confidence);
286
+ const bugId = String(finding.bugId || '').trim();
287
+ const key = `${file}|${lines}|${claim}`;
288
+ const existing = state.bugLedger.find((entry) => entry.key === key);
289
+ if (!existing) {
290
+ state.bugLedger.push({
291
+ key,
292
+ bugId,
293
+ severity,
294
+ file,
295
+ lines,
296
+ claim,
297
+ confidence,
298
+ status: 'open',
299
+ source,
300
+ updatedAt: nowIso()
301
+ });
302
+ inserted += 1;
303
+ continue;
304
+ }
305
+ const existingRank = severityRank(existing.severity);
306
+ const incomingRank = severityRank(severity);
307
+ if (incomingRank > existingRank) {
308
+ existing.severity = severity;
309
+ }
310
+ if (!existing.bugId && bugId) {
311
+ existing.bugId = bugId;
312
+ }
313
+ if (existing.confidence === null && confidence !== null) {
314
+ existing.confidence = confidence;
315
+ } else if (existing.confidence !== null && confidence !== null) {
316
+ existing.confidence = Math.max(existing.confidence, confidence);
317
+ }
318
+ existing.updatedAt = nowIso();
319
+ existing.source = source;
320
+ updated += 1;
321
+ }
322
+
323
+ state.metrics.findingsTotal += findings.length;
324
+ state.metrics.findingsUnique = state.bugLedger.length;
325
+ state.metrics.lowConfidenceFindings = state.bugLedger.filter((entry) => {
326
+ return entry.confidence === null || entry.confidence < 75;
327
+ }).length;
328
+ saveState(statePath, state);
329
+ console.log(JSON.stringify({
330
+ ok: true,
331
+ inserted,
332
+ updated,
333
+ metrics: state.metrics
334
+ }, null, 2));
335
+ return;
336
+ }
337
+
338
+ if (command === 'hash-filter') {
339
+ const [statePath, filesJsonPath] = args;
340
+ if (!statePath || !filesJsonPath) {
341
+ usage();
342
+ process.exit(1);
343
+ }
344
+ const state = readState(statePath);
345
+ const files = readJson(filesJsonPath);
346
+ assertArray(files, 'filesJson');
347
+
348
+ const scan = [];
349
+ const skip = [];
350
+ const missing = [];
351
+
352
+ for (const filePath of files) {
353
+ const normalized = String(filePath);
354
+ if (!fs.existsSync(normalized)) {
355
+ missing.push(normalized);
356
+ continue;
357
+ }
358
+ const currentHash = hashFile(normalized);
359
+ const previous = state.hashCache[normalized];
360
+ if (previous && previous.hash === currentHash) {
361
+ skip.push(normalized);
362
+ } else {
363
+ scan.push(normalized);
364
+ }
365
+ }
366
+
367
+ console.log(JSON.stringify({ ok: true, scan, skip, missing }, null, 2));
368
+ return;
369
+ }
370
+
371
+ if (command === 'hash-update') {
372
+ const [statePath, filesJsonPath, cacheStatus = 'scanned'] = args;
373
+ if (!statePath || !filesJsonPath) {
374
+ usage();
375
+ process.exit(1);
376
+ }
377
+ const state = readState(statePath);
378
+ const files = readJson(filesJsonPath);
379
+ assertArray(files, 'filesJson');
380
+ const updatedFiles = [];
381
+ const missing = [];
382
+
383
+ for (const filePath of files) {
384
+ const normalized = String(filePath);
385
+ if (!fs.existsSync(normalized)) {
386
+ missing.push(normalized);
387
+ continue;
388
+ }
389
+ state.hashCache[normalized] = {
390
+ hash: hashFile(normalized),
391
+ status: cacheStatus,
392
+ scannedAt: nowIso()
393
+ };
394
+ updatedFiles.push(normalized);
395
+ }
396
+
397
+ saveState(statePath, state);
398
+ console.log(JSON.stringify({
399
+ ok: true,
400
+ updated: updatedFiles.length,
401
+ missing,
402
+ updatedFiles
403
+ }, null, 2));
404
+ return;
405
+ }
406
+
407
+ if (command === 'append-files') {
408
+ const [statePath, filesJsonPath] = args;
409
+ if (!statePath || !filesJsonPath) {
410
+ usage();
411
+ process.exit(1);
412
+ }
413
+ const state = readState(statePath);
414
+ const files = readJson(filesJsonPath);
415
+ assertArray(files, 'filesJson');
416
+ const existing = new Set(state.chunks.flatMap((chunk) => chunk.files));
417
+ const toAppend = [...new Set(files.map((filePath) => String(filePath)))]
418
+ .filter((filePath) => !existing.has(filePath))
419
+ .sort();
420
+ if (toAppend.length === 0) {
421
+ console.log(JSON.stringify({ ok: true, appended: 0, chunksAdded: 0 }, null, 2));
422
+ return;
423
+ }
424
+
425
+ const chunkNumberStart = nextChunkNumber(state.chunks);
426
+ const newChunks = splitChunks(toAppend, state.chunkSize)
427
+ .map((chunk, index) => {
428
+ return {
429
+ ...chunk,
430
+ id: `chunk-${chunkNumberStart + index}`
431
+ };
432
+ });
433
+ state.chunks.push(...newChunks);
434
+ state.metrics.filesTotal += toAppend.length;
435
+ state.metrics.chunksTotal = state.chunks.length;
436
+ saveState(statePath, state);
437
+ console.log(JSON.stringify({
438
+ ok: true,
439
+ appended: toAppend.length,
440
+ chunksAdded: newChunks.length,
441
+ summary: summarize(state)
442
+ }, null, 2));
443
+ return;
444
+ }
445
+
446
+ if (command === 'record-fact-card') {
447
+ const [statePath, chunkId, factCardJsonPath] = args;
448
+ if (!statePath || !chunkId || !factCardJsonPath) {
449
+ usage();
450
+ process.exit(1);
451
+ }
452
+ const state = readState(statePath);
453
+ const factCard = readJson(factCardJsonPath);
454
+ if (!state.factCards || typeof state.factCards !== 'object') {
455
+ state.factCards = {};
456
+ }
457
+ state.factCards[chunkId] = {
458
+ chunkId,
459
+ updatedAt: nowIso(),
460
+ apiContracts: Array.isArray(factCard.apiContracts) ? factCard.apiContracts : [],
461
+ authAssumptions: Array.isArray(factCard.authAssumptions) ? factCard.authAssumptions : [],
462
+ invariants: Array.isArray(factCard.invariants) ? factCard.invariants : []
463
+ };
464
+ saveState(statePath, state);
465
+ console.log(JSON.stringify({
466
+ ok: true,
467
+ chunkId,
468
+ factCards: Object.keys(state.factCards).length
469
+ }, null, 2));
470
+ return;
471
+ }
472
+
473
+ if (command === 'set-consistency') {
474
+ const [statePath, consistencyJsonPath] = args;
475
+ if (!statePath || !consistencyJsonPath) {
476
+ usage();
477
+ process.exit(1);
478
+ }
479
+ const state = readState(statePath);
480
+ const consistency = readJson(consistencyJsonPath);
481
+ state.consistency = consistency;
482
+ saveState(statePath, state);
483
+ console.log(JSON.stringify({
484
+ ok: true,
485
+ consistency
486
+ }, null, 2));
487
+ return;
488
+ }
489
+
490
+ if (command === 'set-fix-plan') {
491
+ const [statePath, fixPlanJsonPath] = args;
492
+ if (!statePath || !fixPlanJsonPath) {
493
+ usage();
494
+ process.exit(1);
495
+ }
496
+ const state = readState(statePath);
497
+ const fixPlan = readJson(fixPlanJsonPath);
498
+ state.fixPlan = fixPlan;
499
+ saveState(statePath, state);
500
+ console.log(JSON.stringify({
501
+ ok: true,
502
+ fixPlan
503
+ }, null, 2));
504
+ return;
505
+ }
506
+
507
+ if (command === 'set-parallel-disabled') {
508
+ const [statePath, boolValue] = args;
509
+ if (!statePath || !boolValue) {
510
+ usage();
511
+ process.exit(1);
512
+ }
513
+ const state = readState(statePath);
514
+ const normalized = String(boolValue).toLowerCase();
515
+ if (normalized !== 'true' && normalized !== 'false') {
516
+ throw new Error('set-parallel-disabled expects true or false');
517
+ }
518
+ state.runtime.parallelDisabled = normalized === 'true';
519
+ saveState(statePath, state);
520
+ console.log(JSON.stringify({
521
+ ok: true,
522
+ runtime: state.runtime
523
+ }, null, 2));
524
+ return;
525
+ }
526
+
527
+ usage();
528
+ process.exit(1);
529
+ }
530
+
531
+ try {
532
+ main();
533
+ } catch (error) {
534
+ const message = error instanceof Error ? error.message : String(error);
535
+ console.error(message);
536
+ process.exit(1);
537
+ }