@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,297 @@
1
+ const assert = require('node:assert/strict');
2
+ const { execFileSync } = require('child_process');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const test = require('node:test');
6
+
7
+ const {
8
+ makeSandbox,
9
+ resolveSkillScript,
10
+ runJson,
11
+ runRaw
12
+ } = require('./test-utils.cjs');
13
+
14
+ const SCRIPT = resolveSkillScript('worktree-harvest.cjs');
15
+
16
+ /** Create a bare git repo + a working clone with a fix branch. */
17
+ function makeGitFixture() {
18
+ const sandbox = makeSandbox('wt-harvest-');
19
+
20
+ // Bare origin
21
+ const origin = path.join(sandbox, 'origin.git');
22
+ fs.mkdirSync(origin, { recursive: true });
23
+ execFileSync('git', ['init', '--bare'], { cwd: origin, stdio: 'ignore' });
24
+
25
+ // Working clone
26
+ const repo = path.join(sandbox, 'repo');
27
+ execFileSync('git', ['clone', origin, repo], { stdio: 'ignore' });
28
+ execFileSync('git', ['config', 'user.email', 'test@test.com'], { cwd: repo, stdio: 'ignore' });
29
+ execFileSync('git', ['config', 'user.name', 'Test'], { cwd: repo, stdio: 'ignore' });
30
+
31
+ // Initial commit on main
32
+ fs.writeFileSync(path.join(repo, 'file.txt'), 'hello\n');
33
+ execFileSync('git', ['add', 'file.txt'], { cwd: repo, stdio: 'ignore' });
34
+ execFileSync('git', ['commit', '-m', 'initial'], { cwd: repo, stdio: 'ignore' });
35
+
36
+ // Create fix branch
37
+ const fixBranch = 'bug-hunter-fix-test';
38
+ execFileSync('git', ['checkout', '-b', fixBranch], { cwd: repo, stdio: 'ignore' });
39
+ // Go back to main so fix branch isn't checked out
40
+ execFileSync('git', ['checkout', 'main'], { cwd: repo, stdio: 'ignore' });
41
+
42
+ return { sandbox, repo, fixBranch };
43
+ }
44
+
45
+ test('prepare creates worktree on fix branch', () => {
46
+ const { repo, fixBranch } = makeGitFixture();
47
+ const wtDir = path.join(repo, '.bug-hunter', 'worktrees', 'batch-1');
48
+
49
+ const result = runJson('node', [SCRIPT, 'prepare', fixBranch, wtDir], { cwd: repo });
50
+ assert.equal(result.ok, true);
51
+ assert.equal(result.fixBranch, fixBranch);
52
+ assert.ok(result.preHarvestHead);
53
+ assert.ok(fs.existsSync(wtDir));
54
+
55
+ // Verify worktree is on fix branch
56
+ const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
57
+ cwd: wtDir, encoding: 'utf8'
58
+ }).trim();
59
+ assert.equal(branch, fixBranch);
60
+
61
+ // Verify manifest
62
+ const manifest = JSON.parse(fs.readFileSync(path.join(wtDir, '.worktree-manifest.json'), 'utf8'));
63
+ assert.equal(manifest.fixBranch, fixBranch);
64
+ assert.ok(manifest.preHarvestHead);
65
+ });
66
+
67
+ test('prepare detaches main tree when on fix branch', () => {
68
+ const { repo, fixBranch } = makeGitFixture();
69
+ // Check out fix branch on main tree
70
+ execFileSync('git', ['checkout', fixBranch], { cwd: repo, stdio: 'ignore' });
71
+
72
+ const wtDir = path.join(repo, '.bug-hunter', 'worktrees', 'batch-1');
73
+ const result = runJson('node', [SCRIPT, 'prepare', fixBranch, wtDir], { cwd: repo });
74
+ assert.equal(result.ok, true);
75
+ assert.equal(result.detachedMainTree, true);
76
+
77
+ // Main tree should be detached
78
+ const mainBranch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
79
+ cwd: repo, encoding: 'utf8'
80
+ }).trim();
81
+ assert.equal(mainBranch, 'HEAD'); // detached
82
+ });
83
+
84
+ test('prepare fails for non-existent branch', () => {
85
+ const { repo } = makeGitFixture();
86
+ const wtDir = path.join(repo, '.bug-hunter', 'worktrees', 'batch-1');
87
+
88
+ const result = runRaw('node', [SCRIPT, 'prepare', 'no-such-branch', wtDir], { cwd: repo });
89
+ assert.notEqual(result.status, 0);
90
+ const output = `${result.stdout || ''}`;
91
+ assert.match(output, /fix-branch-not-found/);
92
+ });
93
+
94
+ test('prepare cleans up stale worktree', () => {
95
+ const { repo, fixBranch } = makeGitFixture();
96
+ const wtDir = path.join(repo, '.bug-hunter', 'worktrees', 'batch-1');
97
+
98
+ // Create first worktree
99
+ runJson('node', [SCRIPT, 'prepare', fixBranch, wtDir], { cwd: repo });
100
+ // Harvest + cleanup the first one so the branch is free
101
+ runJson('node', [SCRIPT, 'harvest', wtDir], { cwd: repo });
102
+ runJson('node', [SCRIPT, 'cleanup', wtDir], { cwd: repo });
103
+
104
+ // Create second one on same path — should succeed
105
+ const result = runJson('node', [SCRIPT, 'prepare', fixBranch, wtDir], { cwd: repo });
106
+ assert.equal(result.ok, true);
107
+ });
108
+
109
+ test('harvest finds new commits', () => {
110
+ const { repo, fixBranch } = makeGitFixture();
111
+ const wtDir = path.join(repo, '.bug-hunter', 'worktrees', 'batch-1');
112
+
113
+ runJson('node', [SCRIPT, 'prepare', fixBranch, wtDir], { cwd: repo });
114
+
115
+ // Simulate fixer: edit and commit inside worktree
116
+ fs.writeFileSync(path.join(wtDir, 'fix.txt'), 'patched\n');
117
+ execFileSync('git', ['add', 'fix.txt'], { cwd: wtDir, stdio: 'ignore' });
118
+ execFileSync('git', ['commit', '-m', 'fix(bug-hunter): BUG-3 — add input validation'], {
119
+ cwd: wtDir, stdio: 'ignore'
120
+ });
121
+
122
+ const result = runJson('node', [SCRIPT, 'harvest', wtDir], { cwd: repo });
123
+ assert.equal(result.ok, true);
124
+ assert.equal(result.harvestedCount, 1);
125
+ assert.equal(result.commits[0].bugId, 'BUG-3');
126
+ assert.equal(result.noChanges, false);
127
+ assert.equal(result.uncommittedStashed, false);
128
+ });
129
+
130
+ test('harvest handles dirty state — stashes uncommitted work', () => {
131
+ const { repo, fixBranch } = makeGitFixture();
132
+ const wtDir = path.join(repo, '.bug-hunter', 'worktrees', 'batch-1');
133
+
134
+ runJson('node', [SCRIPT, 'prepare', fixBranch, wtDir], { cwd: repo });
135
+
136
+ // Simulate fixer that edits but doesn't commit
137
+ fs.writeFileSync(path.join(wtDir, 'dirty.txt'), 'uncommitted\n');
138
+
139
+ const result = runJson('node', [SCRIPT, 'harvest', wtDir], { cwd: repo });
140
+ assert.equal(result.ok, true);
141
+ assert.equal(result.harvestedCount, 0);
142
+ assert.equal(result.uncommittedStashed, true);
143
+ assert.ok(result.stashRef);
144
+ });
145
+
146
+ test('harvest handles no-op — clean worktree with no changes', () => {
147
+ const { repo, fixBranch } = makeGitFixture();
148
+ const wtDir = path.join(repo, '.bug-hunter', 'worktrees', 'batch-1');
149
+
150
+ runJson('node', [SCRIPT, 'prepare', fixBranch, wtDir], { cwd: repo });
151
+
152
+ // No changes at all
153
+ const result = runJson('node', [SCRIPT, 'harvest', wtDir], { cwd: repo });
154
+ assert.equal(result.ok, true);
155
+ assert.equal(result.harvestedCount, 0);
156
+ assert.equal(result.noChanges, true);
157
+ assert.equal(result.uncommittedStashed, false);
158
+ });
159
+
160
+ test('cleanup removes worktree', () => {
161
+ const { repo, fixBranch } = makeGitFixture();
162
+ const wtDir = path.join(repo, '.bug-hunter', 'worktrees', 'batch-1');
163
+
164
+ runJson('node', [SCRIPT, 'prepare', fixBranch, wtDir], { cwd: repo });
165
+ runJson('node', [SCRIPT, 'harvest', wtDir], { cwd: repo });
166
+
167
+ const result = runJson('node', [SCRIPT, 'cleanup', wtDir], { cwd: repo });
168
+ assert.equal(result.ok, true);
169
+ assert.equal(result.removed, true);
170
+ assert.ok(!fs.existsSync(wtDir));
171
+ });
172
+
173
+ test('cleanup handles missing worktree gracefully', () => {
174
+ const { repo } = makeGitFixture();
175
+ const wtDir = path.join(repo, '.bug-hunter', 'worktrees', 'nonexistent');
176
+
177
+ const result = runJson('node', [SCRIPT, 'cleanup', wtDir], { cwd: repo });
178
+ assert.equal(result.ok, true);
179
+ assert.equal(result.removed, false);
180
+ assert.equal(result.reason, 'not-found');
181
+ });
182
+
183
+ test('cleanup-all removes multiple worktrees', () => {
184
+ const { repo, fixBranch } = makeGitFixture();
185
+ const parentDir = path.join(repo, '.bug-hunter', 'worktrees');
186
+ const wt1 = path.join(parentDir, 'batch-1');
187
+ const wt2 = path.join(parentDir, 'batch-2');
188
+
189
+ // Create two worktrees sequentially (one at a time on the fix branch)
190
+ runJson('node', [SCRIPT, 'prepare', fixBranch, wt1], { cwd: repo });
191
+ runJson('node', [SCRIPT, 'harvest', wt1], { cwd: repo });
192
+ runJson('node', [SCRIPT, 'cleanup', wt1], { cwd: repo });
193
+
194
+ runJson('node', [SCRIPT, 'prepare', fixBranch, wt2], { cwd: repo });
195
+
196
+ // Now simulate crash — wt2 still exists. Cleanup-all should handle it.
197
+ // First recreate wt1 dir as if it's stale leftover
198
+ fs.mkdirSync(wt1, { recursive: true });
199
+
200
+ const result = runJson('node', [SCRIPT, 'cleanup-all', parentDir], { cwd: repo });
201
+ assert.equal(result.ok, true);
202
+ assert.ok(result.cleaned >= 1);
203
+ });
204
+
205
+ test('checkout-fix returns main tree to fix branch', () => {
206
+ const { repo, fixBranch } = makeGitFixture();
207
+ const wtDir = path.join(repo, '.bug-hunter', 'worktrees', 'batch-1');
208
+
209
+ // Prepare (may detach main tree)
210
+ runJson('node', [SCRIPT, 'prepare', fixBranch, wtDir], { cwd: repo });
211
+ runJson('node', [SCRIPT, 'harvest', wtDir], { cwd: repo });
212
+ runJson('node', [SCRIPT, 'cleanup', wtDir], { cwd: repo });
213
+
214
+ // Now checkout fix branch on main tree
215
+ const result = runJson('node', [SCRIPT, 'checkout-fix', fixBranch], { cwd: repo });
216
+ assert.equal(result.ok, true);
217
+ assert.equal(result.branch, fixBranch);
218
+ assert.ok(result.head);
219
+
220
+ // Verify
221
+ const branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
222
+ cwd: repo, encoding: 'utf8'
223
+ }).trim();
224
+ assert.equal(branch, fixBranch);
225
+ });
226
+
227
+ test('full lifecycle: prepare → commit → harvest → cleanup → checkout-fix', () => {
228
+ const { repo, fixBranch } = makeGitFixture();
229
+ const wtDir = path.join(repo, '.bug-hunter', 'worktrees', 'batch-1');
230
+
231
+ // 1. Prepare
232
+ const prep = runJson('node', [SCRIPT, 'prepare', fixBranch, wtDir], { cwd: repo });
233
+ assert.equal(prep.ok, true);
234
+
235
+ // 2. Fixer commits two bugs
236
+ fs.writeFileSync(path.join(wtDir, 'auth.ts'), 'fixed auth\n');
237
+ execFileSync('git', ['add', 'auth.ts'], { cwd: wtDir, stdio: 'ignore' });
238
+ execFileSync('git', ['commit', '-m', 'fix(bug-hunter): BUG-1 — SQL injection fix'], {
239
+ cwd: wtDir, stdio: 'ignore'
240
+ });
241
+
242
+ fs.writeFileSync(path.join(wtDir, 'api.ts'), 'fixed api\n');
243
+ execFileSync('git', ['add', 'api.ts'], { cwd: wtDir, stdio: 'ignore' });
244
+ execFileSync('git', ['commit', '-m', 'fix(bug-hunter): BUG-2 — XSS prevention'], {
245
+ cwd: wtDir, stdio: 'ignore'
246
+ });
247
+
248
+ // 3. Harvest
249
+ const harv = runJson('node', [SCRIPT, 'harvest', wtDir], { cwd: repo });
250
+ assert.equal(harv.ok, true);
251
+ assert.equal(harv.harvestedCount, 2);
252
+ assert.equal(harv.commits[0].bugId, 'BUG-1');
253
+ assert.equal(harv.commits[1].bugId, 'BUG-2');
254
+
255
+ // 4. Cleanup
256
+ const clean = runJson('node', [SCRIPT, 'cleanup', wtDir], { cwd: repo });
257
+ assert.equal(clean.ok, true);
258
+ assert.equal(clean.removed, true);
259
+
260
+ // 5. Checkout fix branch on main tree
261
+ const co = runJson('node', [SCRIPT, 'checkout-fix', fixBranch], { cwd: repo });
262
+ assert.equal(co.ok, true);
263
+
264
+ // 6. Verify commits are on the fix branch in main tree
265
+ const log = execFileSync('git', ['log', '--oneline', 'main..HEAD'], {
266
+ cwd: repo, encoding: 'utf8'
267
+ }).trim();
268
+ assert.match(log, /BUG-1/);
269
+ assert.match(log, /BUG-2/);
270
+
271
+ // 7. Verify files exist in main tree
272
+ assert.ok(fs.existsSync(path.join(repo, 'auth.ts')));
273
+ assert.ok(fs.existsSync(path.join(repo, 'api.ts')));
274
+ });
275
+
276
+ test('status reports worktree health', () => {
277
+ const { repo, fixBranch } = makeGitFixture();
278
+ const wtDir = path.join(repo, '.bug-hunter', 'worktrees', 'batch-1');
279
+
280
+ // Non-existent
281
+ const s1 = runJson('node', [SCRIPT, 'status', wtDir], { cwd: repo });
282
+ assert.equal(s1.exists, false);
283
+
284
+ // Create worktree
285
+ runJson('node', [SCRIPT, 'prepare', fixBranch, wtDir], { cwd: repo });
286
+
287
+ const s2 = runJson('node', [SCRIPT, 'status', wtDir], { cwd: repo });
288
+ assert.equal(s2.exists, true);
289
+ assert.equal(s2.branch, fixBranch);
290
+ assert.equal(s2.isStale, false);
291
+ assert.equal(s2.commitCount, 0);
292
+ assert.equal(s2.harvested, false);
293
+
294
+ // Clean up
295
+ runJson('node', [SCRIPT, 'harvest', wtDir], { cwd: repo });
296
+ runJson('node', [SCRIPT, 'cleanup', wtDir], { cwd: repo });
297
+ });