@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.
- package/CHANGELOG.md +151 -0
- package/LICENSE +21 -0
- package/README.md +665 -0
- package/SKILL.md +624 -0
- package/bin/bug-hunter +222 -0
- package/evals/evals.json +362 -0
- package/modes/_dispatch.md +121 -0
- package/modes/extended.md +94 -0
- package/modes/fix-loop.md +115 -0
- package/modes/fix-pipeline.md +384 -0
- package/modes/large-codebase.md +212 -0
- package/modes/local-sequential.md +143 -0
- package/modes/loop.md +125 -0
- package/modes/parallel.md +113 -0
- package/modes/scaled.md +76 -0
- package/modes/single-file.md +38 -0
- package/modes/small.md +86 -0
- package/package.json +56 -0
- package/prompts/doc-lookup.md +44 -0
- package/prompts/examples/hunter-examples.md +131 -0
- package/prompts/examples/skeptic-examples.md +87 -0
- package/prompts/fixer.md +103 -0
- package/prompts/hunter.md +146 -0
- package/prompts/recon.md +159 -0
- package/prompts/referee.md +122 -0
- package/prompts/skeptic.md +143 -0
- package/prompts/threat-model.md +122 -0
- package/scripts/bug-hunter-state.cjs +537 -0
- package/scripts/code-index.cjs +541 -0
- package/scripts/context7-api.cjs +133 -0
- package/scripts/delta-mode.cjs +219 -0
- package/scripts/dep-scan.cjs +343 -0
- package/scripts/doc-lookup.cjs +316 -0
- package/scripts/fix-lock.cjs +167 -0
- package/scripts/init-test-fixture.sh +19 -0
- package/scripts/payload-guard.cjs +197 -0
- package/scripts/run-bug-hunter.cjs +892 -0
- package/scripts/tests/bug-hunter-state.test.cjs +87 -0
- package/scripts/tests/code-index.test.cjs +57 -0
- package/scripts/tests/delta-mode.test.cjs +47 -0
- package/scripts/tests/fix-lock.test.cjs +36 -0
- package/scripts/tests/fixtures/flaky-worker.cjs +63 -0
- package/scripts/tests/fixtures/low-confidence-worker.cjs +73 -0
- package/scripts/tests/fixtures/success-worker.cjs +42 -0
- package/scripts/tests/payload-guard.test.cjs +41 -0
- package/scripts/tests/run-bug-hunter.test.cjs +403 -0
- package/scripts/tests/test-utils.cjs +59 -0
- package/scripts/tests/worktree-harvest.test.cjs +297 -0
- package/scripts/triage.cjs +528 -0
- package/scripts/worktree-harvest.cjs +516 -0
- 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
|
+
});
|