@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,516 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* worktree-harvest.cjs — Worktree lifecycle manager for bug-hunter fix pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Manages isolated git worktrees for Fixer subagents:
|
|
7
|
+
* prepare → create worktree on the fix branch
|
|
8
|
+
* harvest → validate Fixer commits, detect uncommitted work
|
|
9
|
+
* checkout-fix → return main working tree to the fix branch
|
|
10
|
+
* cleanup → remove a single worktree
|
|
11
|
+
* cleanup-all → remove all worktrees under a directory
|
|
12
|
+
* status → report worktree health
|
|
13
|
+
*
|
|
14
|
+
* Design (inspired by Droid Mission Control):
|
|
15
|
+
* Workers are process-isolated but commit to the SAME branch.
|
|
16
|
+
* The worktree checks out the fix branch directly — no cherry-picking.
|
|
17
|
+
* The orchestrator manages the lifecycle: prepare → dispatch → harvest → cleanup.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { execFileSync } = require('child_process');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Helpers
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
function usage() {
|
|
29
|
+
console.error('Usage:');
|
|
30
|
+
console.error(' worktree-harvest.cjs prepare <fixBranch> <worktreeDir>');
|
|
31
|
+
console.error(' worktree-harvest.cjs harvest <worktreeDir>');
|
|
32
|
+
console.error(' worktree-harvest.cjs checkout-fix <fixBranch>');
|
|
33
|
+
console.error(' worktree-harvest.cjs cleanup <worktreeDir>');
|
|
34
|
+
console.error(' worktree-harvest.cjs cleanup-all <parentDir>');
|
|
35
|
+
console.error(' worktree-harvest.cjs status <worktreeDir>');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function out(obj) {
|
|
39
|
+
console.log(JSON.stringify(obj, null, 2));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Run git with execFileSync — no shell, no injection risk. */
|
|
43
|
+
function git(args, cwd) {
|
|
44
|
+
const opts = { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] };
|
|
45
|
+
if (cwd) opts.cwd = cwd;
|
|
46
|
+
return execFileSync('git', args, opts).trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Same as git() but returns { ok, output } instead of throwing. */
|
|
50
|
+
function gitSafe(args, cwd) {
|
|
51
|
+
try {
|
|
52
|
+
return { ok: true, output: git(args, cwd) };
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const stderr = err.stderr ? err.stderr.toString().trim() : '';
|
|
55
|
+
return { ok: false, output: stderr || (err.message || '').trim() };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ensureDir(dir) {
|
|
60
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const MANIFEST_NAME = '.worktree-manifest.json';
|
|
64
|
+
const HARVEST_NAME = '.harvest-result.json';
|
|
65
|
+
const STALE_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
66
|
+
|
|
67
|
+
function manifestPath(worktreeDir) {
|
|
68
|
+
return path.join(worktreeDir, MANIFEST_NAME);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function harvestPath(worktreeDir) {
|
|
72
|
+
return path.join(worktreeDir, HARVEST_NAME);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readJsonFile(filePath) {
|
|
76
|
+
if (!fs.existsSync(filePath)) return null;
|
|
77
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeJsonFile(filePath, data) {
|
|
81
|
+
ensureDir(path.dirname(filePath));
|
|
82
|
+
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// prepare — create worktree on the fix branch
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
function prepare(fixBranch, worktreeDir) {
|
|
90
|
+
const absDir = path.resolve(worktreeDir);
|
|
91
|
+
|
|
92
|
+
// 1. Verify fix branch exists
|
|
93
|
+
const branchCheck = gitSafe(['rev-parse', '--verify', fixBranch]);
|
|
94
|
+
if (!branchCheck.ok) {
|
|
95
|
+
out({ ok: false, error: 'fix-branch-not-found', detail: branchCheck.output });
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 2. If worktreeDir already exists, clean up stale worktree
|
|
100
|
+
if (fs.existsSync(absDir)) {
|
|
101
|
+
gitSafe(['worktree', 'remove', absDir, '--force']);
|
|
102
|
+
if (fs.existsSync(absDir)) {
|
|
103
|
+
fs.rmSync(absDir, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
gitSafe(['worktree', 'prune']);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 3. Detach main working tree if it's on the fix branch
|
|
109
|
+
// (git won't allow two worktrees on the same branch)
|
|
110
|
+
const currentBranch = gitSafe(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
111
|
+
let detached = false;
|
|
112
|
+
if (currentBranch.ok && currentBranch.output === fixBranch) {
|
|
113
|
+
git(['checkout', '--detach']);
|
|
114
|
+
detached = true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 4. Create worktree on the fix branch
|
|
118
|
+
ensureDir(path.dirname(absDir));
|
|
119
|
+
const addResult = gitSafe(['worktree', 'add', absDir, fixBranch]);
|
|
120
|
+
if (!addResult.ok) {
|
|
121
|
+
// Restore branch if we detached
|
|
122
|
+
if (detached) gitSafe(['checkout', fixBranch]);
|
|
123
|
+
out({ ok: false, error: 'worktree-add-failed', detail: addResult.output });
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 5. Record pre-harvest HEAD
|
|
128
|
+
const preHarvestHead = git(['rev-parse', 'HEAD'], absDir);
|
|
129
|
+
|
|
130
|
+
// 6. Write manifest
|
|
131
|
+
const manifest = {
|
|
132
|
+
fixBranch,
|
|
133
|
+
preHarvestHead,
|
|
134
|
+
worktreeDir: absDir,
|
|
135
|
+
detachedMainTree: detached,
|
|
136
|
+
createdAtMs: Date.now(),
|
|
137
|
+
createdAt: new Date().toISOString()
|
|
138
|
+
};
|
|
139
|
+
writeJsonFile(manifestPath(absDir), manifest);
|
|
140
|
+
|
|
141
|
+
out({
|
|
142
|
+
ok: true,
|
|
143
|
+
worktreeDir: absDir,
|
|
144
|
+
fixBranch,
|
|
145
|
+
preHarvestHead,
|
|
146
|
+
detachedMainTree: detached,
|
|
147
|
+
createdAt: manifest.createdAt
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
// harvest — validate Fixer commits and detect uncommitted work
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
/** Meta files written into the worktree — excluded from dirty detection. */
|
|
156
|
+
const META_FILES = [MANIFEST_NAME, HARVEST_NAME];
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Core harvest logic. Returns the result object or throws on error.
|
|
160
|
+
* Does NOT call process.exit — safe for internal callers like cleanup-all.
|
|
161
|
+
*/
|
|
162
|
+
function harvestCore(worktreeDir) {
|
|
163
|
+
const absDir = path.resolve(worktreeDir);
|
|
164
|
+
|
|
165
|
+
// 1. Read manifest
|
|
166
|
+
const manifest = readJsonFile(manifestPath(absDir));
|
|
167
|
+
if (!manifest) {
|
|
168
|
+
throw new Error(JSON.stringify({
|
|
169
|
+
ok: false, error: 'no-manifest', detail: `${manifestPath(absDir)} not found`
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { preHarvestHead, fixBranch } = manifest;
|
|
174
|
+
|
|
175
|
+
// 2. Verify worktree is still on the fix branch
|
|
176
|
+
const wtBranch = gitSafe(['rev-parse', '--abbrev-ref', 'HEAD'], absDir);
|
|
177
|
+
if (wtBranch.ok && wtBranch.output !== fixBranch) {
|
|
178
|
+
const result = {
|
|
179
|
+
ok: true,
|
|
180
|
+
error: 'branch-switched',
|
|
181
|
+
detail: `Fixer switched from ${fixBranch} to ${wtBranch.output}`,
|
|
182
|
+
commits: [],
|
|
183
|
+
branchSwitched: true
|
|
184
|
+
};
|
|
185
|
+
writeJsonFile(harvestPath(absDir), result);
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 3. Find new commits since preHarvestHead
|
|
190
|
+
const logResult = gitSafe(
|
|
191
|
+
['log', '--oneline', '--reverse', `${preHarvestHead}..HEAD`],
|
|
192
|
+
absDir
|
|
193
|
+
);
|
|
194
|
+
const logOutput = logResult.ok ? logResult.output : '';
|
|
195
|
+
const commitLines = logOutput ? logOutput.split('\n').filter(Boolean) : [];
|
|
196
|
+
|
|
197
|
+
// 4. Parse commits
|
|
198
|
+
const commits = commitLines.map(line => {
|
|
199
|
+
const spaceIdx = line.indexOf(' ');
|
|
200
|
+
const hash = line.slice(0, spaceIdx);
|
|
201
|
+
const message = line.slice(spaceIdx + 1);
|
|
202
|
+
const bugMatch = message.match(/BUG-(\d+)/);
|
|
203
|
+
return {
|
|
204
|
+
hash,
|
|
205
|
+
message,
|
|
206
|
+
bugId: bugMatch ? `BUG-${bugMatch[1]}` : null
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// 5. Check for uncommitted changes (exclude our own meta files)
|
|
211
|
+
const statusOutput = gitSafe(['status', '--porcelain'], absDir);
|
|
212
|
+
const statusLines = statusOutput.ok
|
|
213
|
+
? statusOutput.output.split('\n').filter(Boolean)
|
|
214
|
+
: [];
|
|
215
|
+
const relevantLines = statusLines.filter(line => {
|
|
216
|
+
const fileName = line.slice(3); // strip status prefix (e.g. "?? ")
|
|
217
|
+
return !META_FILES.some(mf => fileName === mf || fileName.endsWith(`/${mf}`));
|
|
218
|
+
});
|
|
219
|
+
const dirty = relevantLines.length > 0;
|
|
220
|
+
|
|
221
|
+
let uncommittedStashed = false;
|
|
222
|
+
let stashRef = null;
|
|
223
|
+
|
|
224
|
+
if (dirty) {
|
|
225
|
+
// Stash uncommitted work so it's not lost
|
|
226
|
+
const stashMsg = `bug-hunter-fixer-uncommitted-${Date.now()}`;
|
|
227
|
+
gitSafe(['add', '-A'], absDir);
|
|
228
|
+
const stashResult = gitSafe(['stash', 'push', '-m', stashMsg], absDir);
|
|
229
|
+
if (stashResult.ok) {
|
|
230
|
+
uncommittedStashed = true;
|
|
231
|
+
const stashList = gitSafe(['stash', 'list', '--max-count=1'], absDir);
|
|
232
|
+
stashRef = stashList.ok ? stashList.output.split(':')[0] : null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const postHarvestHead = gitSafe(['rev-parse', 'HEAD'], absDir);
|
|
237
|
+
|
|
238
|
+
const result = {
|
|
239
|
+
ok: true,
|
|
240
|
+
commits,
|
|
241
|
+
harvestedCount: commits.length,
|
|
242
|
+
noChanges: commits.length === 0 && !dirty,
|
|
243
|
+
uncommittedStashed,
|
|
244
|
+
stashRef,
|
|
245
|
+
preHarvestHead,
|
|
246
|
+
postHarvestHead: postHarvestHead.ok ? postHarvestHead.output : null
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
writeJsonFile(harvestPath(absDir), result);
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** CLI-facing harvest — prints result and exits on error. */
|
|
254
|
+
function harvest(worktreeDir) {
|
|
255
|
+
try {
|
|
256
|
+
const result = harvestCore(worktreeDir);
|
|
257
|
+
out(result);
|
|
258
|
+
} catch (err) {
|
|
259
|
+
// Error from harvestCore is a JSON string
|
|
260
|
+
try {
|
|
261
|
+
const parsed = JSON.parse(err.message);
|
|
262
|
+
out(parsed);
|
|
263
|
+
} catch (_) {
|
|
264
|
+
out({ ok: false, error: 'harvest-failed', detail: err.message });
|
|
265
|
+
}
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// checkout-fix — return main working tree to the fix branch
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
function checkoutFix(fixBranch) {
|
|
275
|
+
// Verify no non-main worktrees have this branch checked out
|
|
276
|
+
const worktreeList = git(['worktree', 'list', '--porcelain']);
|
|
277
|
+
const entries = worktreeList.split('\n\n').filter(Boolean);
|
|
278
|
+
const mainWorktree = git(['rev-parse', '--show-toplevel']);
|
|
279
|
+
|
|
280
|
+
for (const entry of entries) {
|
|
281
|
+
const lines = entry.split('\n');
|
|
282
|
+
const worktreeLine = lines.find(l => l.startsWith('worktree '));
|
|
283
|
+
const branchLine = lines.find(l => l.startsWith('branch '));
|
|
284
|
+
if (!branchLine || !worktreeLine) continue;
|
|
285
|
+
|
|
286
|
+
const branch = branchLine.replace('branch refs/heads/', '');
|
|
287
|
+
const wtPath = worktreeLine.replace('worktree ', '');
|
|
288
|
+
|
|
289
|
+
if (branch === fixBranch && wtPath !== mainWorktree) {
|
|
290
|
+
out({
|
|
291
|
+
ok: false,
|
|
292
|
+
error: 'worktree-still-active',
|
|
293
|
+
detail: `Branch ${fixBranch} is checked out in worktree: ${wtPath}`
|
|
294
|
+
});
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const result = gitSafe(['checkout', fixBranch]);
|
|
300
|
+
if (!result.ok) {
|
|
301
|
+
out({ ok: false, error: 'checkout-failed', detail: result.output });
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const head = git(['rev-parse', 'HEAD']);
|
|
306
|
+
out({ ok: true, branch: fixBranch, head });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ---------------------------------------------------------------------------
|
|
310
|
+
// cleanup — remove a single worktree
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
function cleanup(worktreeDir) {
|
|
314
|
+
const absDir = path.resolve(worktreeDir);
|
|
315
|
+
|
|
316
|
+
if (!fs.existsSync(absDir)) {
|
|
317
|
+
gitSafe(['worktree', 'prune']);
|
|
318
|
+
out({ ok: true, removed: false, reason: 'not-found' });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// If harvest hasn't run yet, run it defensively
|
|
323
|
+
if (!readJsonFile(harvestPath(absDir))) {
|
|
324
|
+
try { harvestCore(absDir); } catch (_) { /* best-effort */ }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const manifest = readJsonFile(manifestPath(absDir));
|
|
328
|
+
|
|
329
|
+
// Remove worktree
|
|
330
|
+
const removeResult = gitSafe(['worktree', 'remove', absDir, '--force']);
|
|
331
|
+
if (!removeResult.ok && fs.existsSync(absDir)) {
|
|
332
|
+
fs.rmSync(absDir, { recursive: true, force: true });
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
gitSafe(['worktree', 'prune']);
|
|
336
|
+
|
|
337
|
+
out({
|
|
338
|
+
ok: true,
|
|
339
|
+
removed: true,
|
|
340
|
+
detachedMainTree: manifest ? manifest.detachedMainTree : false
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// cleanup-all — remove all worktrees under a parent directory
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
function cleanupAll(parentDir) {
|
|
349
|
+
const absParent = path.resolve(parentDir);
|
|
350
|
+
|
|
351
|
+
if (!fs.existsSync(absParent)) {
|
|
352
|
+
out({ ok: true, cleaned: 0, entries: [] });
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
let entries;
|
|
357
|
+
try {
|
|
358
|
+
entries = fs.readdirSync(absParent, { withFileTypes: true })
|
|
359
|
+
.filter(d => d.isDirectory())
|
|
360
|
+
.map(d => d.name);
|
|
361
|
+
} catch (_) {
|
|
362
|
+
out({ ok: true, cleaned: 0, entries: [] });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const results = [];
|
|
367
|
+
for (const name of entries) {
|
|
368
|
+
const wtDir = path.join(absParent, name);
|
|
369
|
+
try {
|
|
370
|
+
// Defensive harvest before cleanup
|
|
371
|
+
if (!readJsonFile(harvestPath(wtDir))) {
|
|
372
|
+
try { harvestCore(wtDir); } catch (_) { /* best-effort */ }
|
|
373
|
+
}
|
|
374
|
+
gitSafe(['worktree', 'remove', wtDir, '--force']);
|
|
375
|
+
if (fs.existsSync(wtDir)) {
|
|
376
|
+
fs.rmSync(wtDir, { recursive: true, force: true });
|
|
377
|
+
}
|
|
378
|
+
results.push({ name, removed: true });
|
|
379
|
+
} catch (err) {
|
|
380
|
+
results.push({ name, removed: false, error: err.message });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
gitSafe(['worktree', 'prune']);
|
|
385
|
+
|
|
386
|
+
// Remove parent if empty
|
|
387
|
+
try {
|
|
388
|
+
const remaining = fs.readdirSync(absParent);
|
|
389
|
+
if (remaining.length === 0) fs.rmdirSync(absParent);
|
|
390
|
+
} catch (_) { /* ignore */ }
|
|
391
|
+
|
|
392
|
+
out({ ok: true, cleaned: results.filter(r => r.removed).length, entries: results });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// status — report worktree health
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
function statusCmd(worktreeDir) {
|
|
400
|
+
const absDir = path.resolve(worktreeDir);
|
|
401
|
+
|
|
402
|
+
if (!fs.existsSync(absDir)) {
|
|
403
|
+
out({ ok: true, exists: false });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const manifest = readJsonFile(manifestPath(absDir));
|
|
408
|
+
const harvestResult = readJsonFile(harvestPath(absDir));
|
|
409
|
+
const age = manifest ? Date.now() - manifest.createdAtMs : null;
|
|
410
|
+
const isStale = age !== null && age > STALE_AGE_MS;
|
|
411
|
+
|
|
412
|
+
const statusOutput = gitSafe(['status', '--porcelain'], absDir);
|
|
413
|
+
const hasUncommitted = statusOutput.ok && statusOutput.output.length > 0;
|
|
414
|
+
|
|
415
|
+
let commitCount = 0;
|
|
416
|
+
if (manifest) {
|
|
417
|
+
const logResult = gitSafe(
|
|
418
|
+
['log', '--oneline', `${manifest.preHarvestHead}..HEAD`],
|
|
419
|
+
absDir
|
|
420
|
+
);
|
|
421
|
+
if (logResult.ok && logResult.output) {
|
|
422
|
+
commitCount = logResult.output.split('\n').filter(Boolean).length;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const branch = gitSafe(['rev-parse', '--abbrev-ref', 'HEAD'], absDir);
|
|
427
|
+
|
|
428
|
+
out({
|
|
429
|
+
ok: true,
|
|
430
|
+
exists: true,
|
|
431
|
+
branch: branch.ok ? branch.output : null,
|
|
432
|
+
fixBranch: manifest ? manifest.fixBranch : null,
|
|
433
|
+
ageMs: age,
|
|
434
|
+
isStale,
|
|
435
|
+
hasUncommitted,
|
|
436
|
+
commitCount,
|
|
437
|
+
harvested: harvestResult !== null,
|
|
438
|
+
createdAt: manifest ? manifest.createdAt : null
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
// Main
|
|
444
|
+
// ---------------------------------------------------------------------------
|
|
445
|
+
|
|
446
|
+
function main() {
|
|
447
|
+
const args = process.argv.slice(2);
|
|
448
|
+
const command = args[0];
|
|
449
|
+
|
|
450
|
+
if (!command) {
|
|
451
|
+
usage();
|
|
452
|
+
process.exit(1);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
switch (command) {
|
|
456
|
+
case 'prepare':
|
|
457
|
+
if (!args[1] || !args[2]) {
|
|
458
|
+
console.error('prepare requires <fixBranch> <worktreeDir>');
|
|
459
|
+
process.exit(1);
|
|
460
|
+
}
|
|
461
|
+
prepare(args[1], args[2]);
|
|
462
|
+
break;
|
|
463
|
+
|
|
464
|
+
case 'harvest':
|
|
465
|
+
if (!args[1]) {
|
|
466
|
+
console.error('harvest requires <worktreeDir>');
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
harvest(args[1]);
|
|
470
|
+
break;
|
|
471
|
+
|
|
472
|
+
case 'checkout-fix':
|
|
473
|
+
if (!args[1]) {
|
|
474
|
+
console.error('checkout-fix requires <fixBranch>');
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
checkoutFix(args[1]);
|
|
478
|
+
break;
|
|
479
|
+
|
|
480
|
+
case 'cleanup':
|
|
481
|
+
if (!args[1]) {
|
|
482
|
+
console.error('cleanup requires <worktreeDir>');
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
cleanup(args[1]);
|
|
486
|
+
break;
|
|
487
|
+
|
|
488
|
+
case 'cleanup-all':
|
|
489
|
+
if (!args[1]) {
|
|
490
|
+
console.error('cleanup-all requires <parentDir>');
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
cleanupAll(args[1]);
|
|
494
|
+
break;
|
|
495
|
+
|
|
496
|
+
case 'status':
|
|
497
|
+
if (!args[1]) {
|
|
498
|
+
console.error('status requires <worktreeDir>');
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
statusCmd(args[1]);
|
|
502
|
+
break;
|
|
503
|
+
|
|
504
|
+
default:
|
|
505
|
+
usage();
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
main();
|
|
512
|
+
} catch (error) {
|
|
513
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
514
|
+
console.error(message);
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Subagent Task Wrapper Template
|
|
2
|
+
|
|
3
|
+
Use this template when dispatching any bug-hunter subagent via the `subagent` or `teams` tool. Fill in the `{VARIABLES}` before dispatch.
|
|
4
|
+
|
|
5
|
+
The orchestrator (main agent) MUST:
|
|
6
|
+
1. Read the relevant prompt file with the Read tool
|
|
7
|
+
2. Read this template with the Read tool
|
|
8
|
+
3. Fill all `{VARIABLES}` with actual values
|
|
9
|
+
4. Dispatch using the selected `AGENT_BACKEND`
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
You are a specialized analysis agent invoked by a bug-hunting pipeline.
|
|
15
|
+
You operate in your own context window. Your work feeds into a multi-phase
|
|
16
|
+
adversarial review process.
|
|
17
|
+
|
|
18
|
+
## Your Role: {ROLE_NAME}
|
|
19
|
+
|
|
20
|
+
{ROLE_DESCRIPTION}
|
|
21
|
+
|
|
22
|
+
## Your System Prompt
|
|
23
|
+
|
|
24
|
+
---BEGIN SYSTEM PROMPT---
|
|
25
|
+
{PROMPT_CONTENT}
|
|
26
|
+
---END SYSTEM PROMPT---
|
|
27
|
+
|
|
28
|
+
## Non-negotiable Rules
|
|
29
|
+
|
|
30
|
+
- **Stay within scope.** Only analyze the files assigned to you below.
|
|
31
|
+
- **Do NOT fix code.** Do NOT add tests. Report findings only.
|
|
32
|
+
- **Do NOT report style issues**, unused imports, missing types, or refactoring ideas.
|
|
33
|
+
- **Do NOT expand scope.** If you find something interesting outside your assigned files, note it in UNTRACED CROSS-REFS but do not investigate.
|
|
34
|
+
- **Be honest about coverage.** If you run out of context reading files, STOP and report partial coverage in FILES SKIPPED. Do not inflate FILES SCANNED.
|
|
35
|
+
- **Use the output format EXACTLY** as specified in your system prompt.
|
|
36
|
+
- **Write output to the specified file.** The orchestrator reads this file for the next phase.
|
|
37
|
+
- **Stop when done.** Do not continue to other phases or offer next-step suggestions.
|
|
38
|
+
- **NEVER run destructive commands** like `rm -rf`.
|
|
39
|
+
|
|
40
|
+
## Worktree Isolation Rules (Fixer role only)
|
|
41
|
+
|
|
42
|
+
{WORKTREE_RULES}
|
|
43
|
+
|
|
44
|
+
If worktree rules are provided above (non-empty), these apply:
|
|
45
|
+
- You are working in an **isolated git worktree**. Your edits cannot affect the user's main working tree.
|
|
46
|
+
- You **MUST** `git add` and `git commit` each fix before you finish. Uncommitted changes will be lost and marked as `FIX_FAILED`.
|
|
47
|
+
- Commit message format: `fix(bug-hunter): BUG-N — [short description]`
|
|
48
|
+
- Do **NOT** call `EnterWorktree` or `ExitWorktree` tools.
|
|
49
|
+
- Do **NOT** run `git checkout`, `git switch`, or `git branch`.
|
|
50
|
+
- If you encounter a git error, report it in your output and stop. Do not attempt recovery.
|
|
51
|
+
|
|
52
|
+
## Your Assignment
|
|
53
|
+
|
|
54
|
+
---BEGIN ASSIGNMENT---
|
|
55
|
+
|
|
56
|
+
**Scan target:** {TARGET_DESCRIPTION}
|
|
57
|
+
|
|
58
|
+
**SKILL_DIR:** {SKILL_DIR}
|
|
59
|
+
(Use this path for all helper script invocations like `node "$SKILL_DIR/scripts/doc-lookup.cjs"` or the fallback `node "$SKILL_DIR/scripts/context7-api.cjs"`)
|
|
60
|
+
|
|
61
|
+
**Files to scan (in risk-map order):**
|
|
62
|
+
{FILE_LIST}
|
|
63
|
+
|
|
64
|
+
**Risk map:**
|
|
65
|
+
{RISK_MAP}
|
|
66
|
+
|
|
67
|
+
**Tech stack:**
|
|
68
|
+
{TECH_STACK}
|
|
69
|
+
|
|
70
|
+
**Phase-specific context:**
|
|
71
|
+
{PHASE_SPECIFIC_CONTEXT}
|
|
72
|
+
|
|
73
|
+
---END ASSIGNMENT---
|
|
74
|
+
|
|
75
|
+
## Output Requirements
|
|
76
|
+
|
|
77
|
+
**Write your complete output to:** `{OUTPUT_FILE_PATH}`
|
|
78
|
+
|
|
79
|
+
Follow the output format specified in your system prompt EXACTLY.
|
|
80
|
+
The orchestrator will read this file to pass your results to the next pipeline phase.
|
|
81
|
+
|
|
82
|
+
If the file path directory does not exist, create it first:
|
|
83
|
+
```bash
|
|
84
|
+
mkdir -p "$(dirname '{OUTPUT_FILE_PATH}')"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Completion
|
|
88
|
+
|
|
89
|
+
When you have finished your analysis:
|
|
90
|
+
1. Write your report to `{OUTPUT_FILE_PATH}`
|
|
91
|
+
2. Output a brief summary to stdout (one paragraph)
|
|
92
|
+
3. Stop. Do not continue to other phases.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Variable Reference (for the orchestrator)
|
|
97
|
+
|
|
98
|
+
| Variable | Description | Example |
|
|
99
|
+
|----------|-------------|---------|
|
|
100
|
+
| `{ROLE_NAME}` | Agent role identifier | `hunter`, `skeptic`, `referee`, `recon`, `fixer` |
|
|
101
|
+
| `{ROLE_DESCRIPTION}` | One-line role description | "Bug Hunter — find behavioral bugs in source code" |
|
|
102
|
+
| `{PROMPT_CONTENT}` | Full contents of the prompt .md file | Contents of `prompts/hunter.md` |
|
|
103
|
+
| `{TARGET_DESCRIPTION}` | What is being scanned | "FindCoffee monorepo, packages/auth + packages/order" |
|
|
104
|
+
| `{SKILL_DIR}` | Absolute path to the bug-hunter skill directory | `/Users/codex/.agents/skills/bug-hunter` |
|
|
105
|
+
| `{FILE_LIST}` | Newline-separated file paths in scan order | CRITICAL files first, then HIGH, then MEDIUM |
|
|
106
|
+
| `{RISK_MAP}` | Recon output risk classification | From `.bug-hunter/recon.md` |
|
|
107
|
+
| `{TECH_STACK}` | Framework, auth, DB, key dependencies | "Express + JWT + Prisma + Redis" |
|
|
108
|
+
| `{PHASE_SPECIFIC_CONTEXT}` | Extra context for this phase | For Skeptic: the Hunter findings. For Referee: findings + Skeptic challenges. |
|
|
109
|
+
| `{OUTPUT_FILE_PATH}` | Where to write the output | `.bug-hunter/findings.md` |
|