@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,316 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unified Documentation Lookup — Context Hub (chub) + Context7 fallback
|
|
5
|
+
*
|
|
6
|
+
* Tries chub first (curated, annotatable docs), falls back to Context7 API
|
|
7
|
+
* when chub doesn't have the library.
|
|
8
|
+
*
|
|
9
|
+
* CLI interface (drop-in replacement for context7-api.cjs):
|
|
10
|
+
* doc-lookup.cjs search <library> <query>
|
|
11
|
+
* doc-lookup.cjs get <library-or-id> <query> [--lang js|py]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
const https = require('https');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
|
|
19
|
+
// ── Config ──────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const CONTEXT7_API_BASE = 'https://context7.com/api/v2';
|
|
22
|
+
const CONTEXT7_TIMEOUT_MS = 15000;
|
|
23
|
+
const CHUB_TIMEOUT_MS = 10000;
|
|
24
|
+
|
|
25
|
+
// ── Context7 fallback (unchanged from context7-api.cjs) ────────────────────
|
|
26
|
+
|
|
27
|
+
function loadContext7Key() {
|
|
28
|
+
if (process.env.CONTEXT7_API_KEY) return process.env.CONTEXT7_API_KEY;
|
|
29
|
+
const envPath = path.join(__dirname, '.env');
|
|
30
|
+
if (fs.existsSync(envPath)) {
|
|
31
|
+
const content = fs.readFileSync(envPath, 'utf8');
|
|
32
|
+
const match = content.match(/CONTEXT7_API_KEY\s*=\s*(.+)/);
|
|
33
|
+
if (match) return match[1].trim().replace(/^["']|["']$/g, '');
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const CONTEXT7_KEY = loadContext7Key();
|
|
39
|
+
|
|
40
|
+
function context7Request(apiPath, params = {}) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const qs = new URLSearchParams(params).toString();
|
|
43
|
+
const url = `${CONTEXT7_API_BASE}${apiPath}?${qs}`;
|
|
44
|
+
const headers = { 'User-Agent': 'BugHunter-DocLookup/2.0' };
|
|
45
|
+
if (CONTEXT7_KEY) headers['Authorization'] = `Bearer ${CONTEXT7_KEY}`;
|
|
46
|
+
|
|
47
|
+
const req = https.get(url, { headers }, (res) => {
|
|
48
|
+
let data = '';
|
|
49
|
+
res.on('data', (c) => (data += c));
|
|
50
|
+
res.on('end', () => {
|
|
51
|
+
if (res.statusCode === 200) {
|
|
52
|
+
try { resolve(JSON.parse(data)); } catch { resolve(data); }
|
|
53
|
+
} else {
|
|
54
|
+
reject(new Error(`Context7 ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
req.setTimeout(CONTEXT7_TIMEOUT_MS, () => req.destroy(new Error('Context7 timeout')));
|
|
59
|
+
req.on('error', reject);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── chub helpers ────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
function chubAvailable() {
|
|
66
|
+
try {
|
|
67
|
+
execSync('chub --help', { stdio: 'ignore', timeout: 3000 });
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function chubSearch(library) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = execSync(`chub search "${library}" --json`, {
|
|
77
|
+
encoding: 'utf8',
|
|
78
|
+
timeout: CHUB_TIMEOUT_MS,
|
|
79
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
80
|
+
});
|
|
81
|
+
const parsed = JSON.parse(raw);
|
|
82
|
+
return parsed.results || [];
|
|
83
|
+
} catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function chubGet(id, lang) {
|
|
89
|
+
try {
|
|
90
|
+
const langFlag = lang ? ` --lang ${lang}` : '';
|
|
91
|
+
const raw = execSync(`chub get ${id}${langFlag}`, {
|
|
92
|
+
encoding: 'utf8',
|
|
93
|
+
timeout: CHUB_TIMEOUT_MS,
|
|
94
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
95
|
+
});
|
|
96
|
+
return raw;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Detect best language flag for chub ──────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function pickLang(entry, hint) {
|
|
105
|
+
if (!entry.languages || entry.languages.length === 0) return null;
|
|
106
|
+
const langs = entry.languages.map((l) => l.language || l);
|
|
107
|
+
if (hint && langs.includes(hint)) return hint;
|
|
108
|
+
// Prefer JS/TS for bug-hunter (most common web targets)
|
|
109
|
+
for (const pref of ['javascript', 'typescript', 'python']) {
|
|
110
|
+
if (langs.includes(pref)) return pref;
|
|
111
|
+
}
|
|
112
|
+
return langs[0];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Main: search ────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
async function search(library, query) {
|
|
118
|
+
const source = { chub: null, context7: null };
|
|
119
|
+
|
|
120
|
+
// 1. Try chub
|
|
121
|
+
if (chubAvailable()) {
|
|
122
|
+
const results = chubSearch(library);
|
|
123
|
+
if (results.length > 0) {
|
|
124
|
+
source.chub = results.map((r) => ({
|
|
125
|
+
id: r.id,
|
|
126
|
+
name: r.name,
|
|
127
|
+
description: r.description,
|
|
128
|
+
type: r._type || r.type || 'doc',
|
|
129
|
+
languages: r.languages ? r.languages.map((l) => l.language || l) : [],
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 2. Try Context7
|
|
135
|
+
try {
|
|
136
|
+
const c7 = await context7Request('/libs/search', { libraryName: library, query });
|
|
137
|
+
if (c7 && (Array.isArray(c7) ? c7.length > 0 : c7.results?.length > 0)) {
|
|
138
|
+
source.context7 = c7;
|
|
139
|
+
}
|
|
140
|
+
} catch (err) {
|
|
141
|
+
source.context7_error = err.message;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 3. Merge results
|
|
145
|
+
const output = { library, query };
|
|
146
|
+
if (source.chub) {
|
|
147
|
+
output.chub_results = source.chub;
|
|
148
|
+
output.recommended_source = 'chub';
|
|
149
|
+
output.recommended_id = source.chub[0].id;
|
|
150
|
+
output.hint = `Fetch with: doc-lookup.cjs get "${source.chub[0].id}" "${query}"`;
|
|
151
|
+
}
|
|
152
|
+
if (source.context7) {
|
|
153
|
+
output.context7_results = source.context7;
|
|
154
|
+
if (!source.chub) {
|
|
155
|
+
output.recommended_source = 'context7';
|
|
156
|
+
// Context7 returns different shapes — handle both
|
|
157
|
+
const first = Array.isArray(source.context7) ? source.context7[0] : source.context7.results?.[0];
|
|
158
|
+
if (first) {
|
|
159
|
+
const c7Id = first.id || first.libraryId;
|
|
160
|
+
output.recommended_id = c7Id;
|
|
161
|
+
output.hint = `Fetch with: doc-lookup.cjs get "${c7Id}" "${query}" --source context7`;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!source.chub && !source.context7) {
|
|
167
|
+
output.error = 'No results from either source';
|
|
168
|
+
if (source.context7_error) output.context7_error = source.context7_error;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return output;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Main: get (fetch docs) ──────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
async function get(idOrLibrary, query, opts = {}) {
|
|
177
|
+
const forceSource = opts.source; // 'chub' | 'context7' | undefined
|
|
178
|
+
const lang = opts.lang;
|
|
179
|
+
|
|
180
|
+
// If forced to context7, skip chub
|
|
181
|
+
if (forceSource === 'context7') {
|
|
182
|
+
return getFromContext7(idOrLibrary, query);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 1. Try chub first
|
|
186
|
+
if (chubAvailable()) {
|
|
187
|
+
// If id looks like a chub id (contains /), try direct get
|
|
188
|
+
if (idOrLibrary.includes('/') && !idOrLibrary.startsWith('/')) {
|
|
189
|
+
const content = chubGet(idOrLibrary, lang);
|
|
190
|
+
if (content && content.trim().length > 50) {
|
|
191
|
+
return {
|
|
192
|
+
source: 'chub',
|
|
193
|
+
id: idOrLibrary,
|
|
194
|
+
content,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Otherwise search chub for the library
|
|
200
|
+
const results = chubSearch(idOrLibrary);
|
|
201
|
+
if (results.length > 0) {
|
|
202
|
+
const best = results[0];
|
|
203
|
+
const bestLang = pickLang(best, lang);
|
|
204
|
+
const content = chubGet(best.id, bestLang);
|
|
205
|
+
if (content && content.trim().length > 50) {
|
|
206
|
+
return {
|
|
207
|
+
source: 'chub',
|
|
208
|
+
id: best.id,
|
|
209
|
+
lang: bestLang,
|
|
210
|
+
content,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// 2. Fallback to Context7
|
|
217
|
+
if (forceSource === 'chub') {
|
|
218
|
+
return { source: 'chub', error: `No chub docs found for "${idOrLibrary}"` };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return getFromContext7(idOrLibrary, query);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function getFromContext7(libraryId, query) {
|
|
225
|
+
try {
|
|
226
|
+
// If it looks like a Context7 library ID (starts with /), use directly
|
|
227
|
+
// Otherwise search first
|
|
228
|
+
let resolvedId = libraryId;
|
|
229
|
+
if (!libraryId.startsWith('/')) {
|
|
230
|
+
const searchResult = await context7Request('/libs/search', {
|
|
231
|
+
libraryName: libraryId,
|
|
232
|
+
query,
|
|
233
|
+
});
|
|
234
|
+
const results = Array.isArray(searchResult) ? searchResult : searchResult?.results || [];
|
|
235
|
+
if (results.length === 0) {
|
|
236
|
+
return { source: 'context7', error: `No Context7 results for "${libraryId}"` };
|
|
237
|
+
}
|
|
238
|
+
resolvedId = results[0].id || results[0].libraryId;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const docs = await context7Request('/context', {
|
|
242
|
+
libraryId: resolvedId,
|
|
243
|
+
query,
|
|
244
|
+
type: 'json',
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
source: 'context7',
|
|
249
|
+
id: resolvedId,
|
|
250
|
+
content: typeof docs === 'string' ? docs : JSON.stringify(docs, null, 2),
|
|
251
|
+
};
|
|
252
|
+
} catch (err) {
|
|
253
|
+
return { source: 'context7', error: err.message };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── CLI ─────────────────────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
const command = process.argv[2];
|
|
260
|
+
const args = process.argv.slice(3);
|
|
261
|
+
|
|
262
|
+
function parseCliOpts(args) {
|
|
263
|
+
const opts = {};
|
|
264
|
+
const positional = [];
|
|
265
|
+
for (let i = 0; i < args.length; i++) {
|
|
266
|
+
if (args[i] === '--lang' && args[i + 1]) {
|
|
267
|
+
opts.lang = args[++i];
|
|
268
|
+
} else if (args[i] === '--source' && args[i + 1]) {
|
|
269
|
+
opts.source = args[++i];
|
|
270
|
+
} else {
|
|
271
|
+
positional.push(args[i]);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return { positional, opts };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
(async () => {
|
|
278
|
+
if (command === 'search') {
|
|
279
|
+
const [library, ...queryParts] = args;
|
|
280
|
+
const query = queryParts.join(' ');
|
|
281
|
+
if (!library || !query) {
|
|
282
|
+
console.error('Usage: doc-lookup.cjs search <library> <query>');
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
const result = await search(library, query);
|
|
286
|
+
console.log(JSON.stringify(result, null, 2));
|
|
287
|
+
} else if (command === 'get' || command === 'context') {
|
|
288
|
+
// 'context' alias for backward compat with context7-api.cjs
|
|
289
|
+
const { positional, opts } = parseCliOpts(args);
|
|
290
|
+
const [idOrLib, ...queryParts] = positional;
|
|
291
|
+
const query = queryParts.join(' ');
|
|
292
|
+
if (!idOrLib || !query) {
|
|
293
|
+
console.error('Usage: doc-lookup.cjs get <library-or-id> <query> [--lang js|py] [--source chub|context7]');
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
const result = await get(idOrLib, query, opts);
|
|
297
|
+
if (result.content) {
|
|
298
|
+
// If content is already a string (from chub), print directly
|
|
299
|
+
// If it's an object, JSON-stringify
|
|
300
|
+
if (typeof result.content === 'string') {
|
|
301
|
+
console.log(`[Source: ${result.source} | ID: ${result.id}${result.lang ? ' | Lang: ' + result.lang : ''}]\n`);
|
|
302
|
+
console.log(result.content);
|
|
303
|
+
} else {
|
|
304
|
+
console.log(JSON.stringify(result, null, 2));
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
console.log(JSON.stringify(result, null, 2));
|
|
308
|
+
}
|
|
309
|
+
} else {
|
|
310
|
+
console.error('Usage: doc-lookup.cjs <search|get> <args...>');
|
|
311
|
+
console.error(' search <library> <query> — find docs across chub + Context7');
|
|
312
|
+
console.error(' get <id-or-lib> <query> [opts] — fetch docs (chub first, Context7 fallback)');
|
|
313
|
+
console.error(' context <id> <query> — alias for get (backward compat)');
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
})();
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
function usage() {
|
|
8
|
+
console.error('Usage:');
|
|
9
|
+
console.error(' fix-lock.cjs acquire <lockPath> [ttlSeconds]');
|
|
10
|
+
console.error(' fix-lock.cjs renew <lockPath>');
|
|
11
|
+
console.error(' fix-lock.cjs release <lockPath>');
|
|
12
|
+
console.error(' fix-lock.cjs status <lockPath> [ttlSeconds]');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function nowMs() {
|
|
16
|
+
return Date.now();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function ensureParent(filePath) {
|
|
20
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readLock(lockPath) {
|
|
24
|
+
if (!fs.existsSync(lockPath)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pidAlive(pid) {
|
|
31
|
+
try {
|
|
32
|
+
process.kill(pid, 0);
|
|
33
|
+
return true;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
if (error && (error.code === 'ESRCH' || error.code === 'EPERM')) {
|
|
36
|
+
return error.code === 'EPERM';
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function lockIsStale(lockData, ttlSeconds) {
|
|
43
|
+
if (!lockData || typeof lockData.createdAtMs !== 'number') {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
const expired = nowMs() - lockData.createdAtMs > ttlSeconds * 1000;
|
|
47
|
+
return expired;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function writeLock(lockPath) {
|
|
51
|
+
ensureParent(lockPath);
|
|
52
|
+
const lockData = {
|
|
53
|
+
pid: process.pid,
|
|
54
|
+
host: os.hostname(),
|
|
55
|
+
cwd: process.cwd(),
|
|
56
|
+
createdAtMs: nowMs(),
|
|
57
|
+
createdAt: new Date().toISOString()
|
|
58
|
+
};
|
|
59
|
+
fs.writeFileSync(lockPath, `${JSON.stringify(lockData, null, 2)}\n`, 'utf8');
|
|
60
|
+
return lockData;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renew(lockPath) {
|
|
64
|
+
const existing = readLock(lockPath);
|
|
65
|
+
if (!existing) {
|
|
66
|
+
console.log(JSON.stringify({ ok: false, renewed: false, reason: 'no-lock' }, null, 2));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
existing.createdAtMs = nowMs();
|
|
71
|
+
existing.renewedAt = new Date().toISOString();
|
|
72
|
+
fs.writeFileSync(lockPath, `${JSON.stringify(existing, null, 2)}\n`, 'utf8');
|
|
73
|
+
console.log(JSON.stringify({ ok: true, renewed: true, lock: existing }, null, 2));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function acquire(lockPath, ttlSeconds) {
|
|
77
|
+
const existing = readLock(lockPath);
|
|
78
|
+
if (!existing) {
|
|
79
|
+
const lockData = writeLock(lockPath);
|
|
80
|
+
console.log(JSON.stringify({ ok: true, acquired: true, lock: lockData }, null, 2));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!lockIsStale(existing, ttlSeconds)) {
|
|
85
|
+
console.log(JSON.stringify({
|
|
86
|
+
ok: false,
|
|
87
|
+
acquired: false,
|
|
88
|
+
reason: 'lock-held',
|
|
89
|
+
lock: existing
|
|
90
|
+
}, null, 2));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fs.unlinkSync(lockPath);
|
|
95
|
+
const lockData = writeLock(lockPath);
|
|
96
|
+
console.log(JSON.stringify({
|
|
97
|
+
ok: true,
|
|
98
|
+
acquired: true,
|
|
99
|
+
recoveredFromStaleLock: true,
|
|
100
|
+
previousLock: existing,
|
|
101
|
+
lock: lockData
|
|
102
|
+
}, null, 2));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function release(lockPath) {
|
|
106
|
+
const existing = readLock(lockPath);
|
|
107
|
+
if (!existing) {
|
|
108
|
+
console.log(JSON.stringify({ ok: true, released: false, reason: 'no-lock' }, null, 2));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
fs.unlinkSync(lockPath);
|
|
112
|
+
console.log(JSON.stringify({ ok: true, released: true, previousLock: existing }, null, 2));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function status(lockPath, ttlSeconds) {
|
|
116
|
+
const existing = readLock(lockPath);
|
|
117
|
+
if (!existing) {
|
|
118
|
+
console.log(JSON.stringify({ ok: true, exists: false }, null, 2));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const stale = lockIsStale(existing, ttlSeconds);
|
|
122
|
+
const alive = typeof existing.pid === 'number' ? pidAlive(existing.pid) : false;
|
|
123
|
+
console.log(JSON.stringify({
|
|
124
|
+
ok: true,
|
|
125
|
+
exists: true,
|
|
126
|
+
stale,
|
|
127
|
+
ownerAlive: alive,
|
|
128
|
+
lock: existing
|
|
129
|
+
}, null, 2));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function main() {
|
|
133
|
+
const [command, lockPath, ttlRaw] = process.argv.slice(2);
|
|
134
|
+
if (!command || !lockPath) {
|
|
135
|
+
usage();
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
const ttlParsed = Number.parseInt(ttlRaw || '', 10);
|
|
139
|
+
const ttlSeconds = Number.isInteger(ttlParsed) && ttlParsed > 0 ? ttlParsed : 1800;
|
|
140
|
+
|
|
141
|
+
if (command === 'acquire') {
|
|
142
|
+
acquire(lockPath, ttlSeconds);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (command === 'renew') {
|
|
146
|
+
renew(lockPath);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (command === 'release') {
|
|
150
|
+
release(lockPath);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (command === 'status') {
|
|
154
|
+
status(lockPath, ttlSeconds);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
usage();
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
main();
|
|
163
|
+
} catch (error) {
|
|
164
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
165
|
+
console.error(message);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Initialize git repo inside test-fixture for fix-mode testing.
|
|
3
|
+
# Run this before using --fix on the test fixture.
|
|
4
|
+
# The source files ship with the repo; this just creates the .git.
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
FIXTURE_DIR="$(cd "$(dirname "$0")/../test-fixture" && pwd)"
|
|
9
|
+
|
|
10
|
+
if [ -d "$FIXTURE_DIR/.git" ]; then
|
|
11
|
+
echo "test-fixture/.git already exists — skipping init"
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
cd "$FIXTURE_DIR"
|
|
16
|
+
git init
|
|
17
|
+
git add -A
|
|
18
|
+
git commit -m "initial commit: test fixture with planted bugs"
|
|
19
|
+
echo "test-fixture git repo initialized at $FIXTURE_DIR"
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const REQUIRED_BY_ROLE = {
|
|
7
|
+
recon: ['skillDir', 'targetFiles', 'outputSchema'],
|
|
8
|
+
'triage-hunter': ['skillDir', 'targetFiles', 'techStack', 'outputSchema'],
|
|
9
|
+
hunter: ['skillDir', 'targetFiles', 'riskMap', 'techStack', 'outputSchema'],
|
|
10
|
+
skeptic: ['skillDir', 'bugs', 'techStack', 'outputSchema'],
|
|
11
|
+
referee: ['skillDir', 'findings', 'skepticResults', 'outputSchema'],
|
|
12
|
+
fixer: ['skillDir', 'bugs', 'techStack', 'outputSchema']
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const TEMPLATES = {
|
|
16
|
+
recon: {
|
|
17
|
+
skillDir: '/absolute/path/to/bug-hunter',
|
|
18
|
+
targetFiles: ['src/example.ts'],
|
|
19
|
+
outputSchema: { format: 'risk-map', version: 1 }
|
|
20
|
+
},
|
|
21
|
+
'triage-hunter': {
|
|
22
|
+
skillDir: '/absolute/path/to/bug-hunter',
|
|
23
|
+
targetFiles: ['src/example.ts'],
|
|
24
|
+
techStack: { framework: '', auth: '', database: '', dependencies: [] },
|
|
25
|
+
outputSchema: { format: 'triage-findings', version: 1 }
|
|
26
|
+
},
|
|
27
|
+
hunter: {
|
|
28
|
+
skillDir: '/absolute/path/to/bug-hunter',
|
|
29
|
+
targetFiles: ['src/example.ts'],
|
|
30
|
+
riskMap: { critical: [], high: [], medium: [], contextOnly: [] },
|
|
31
|
+
techStack: { framework: '', auth: '', database: '', dependencies: [] },
|
|
32
|
+
outputSchema: { format: 'findings', version: 1 }
|
|
33
|
+
},
|
|
34
|
+
skeptic: {
|
|
35
|
+
skillDir: '/absolute/path/to/bug-hunter',
|
|
36
|
+
bugs: [
|
|
37
|
+
{
|
|
38
|
+
bugId: 'BUG-1',
|
|
39
|
+
severity: 'Critical|Medium|Low',
|
|
40
|
+
file: 'src/example.ts',
|
|
41
|
+
lines: '10-15',
|
|
42
|
+
claim: 'One-sentence description of the bug',
|
|
43
|
+
evidence: 'Exact code quote from the file',
|
|
44
|
+
runtimeTrigger: 'Specific scenario that triggers this bug',
|
|
45
|
+
crossReferences: 'Other files involved, or "Single file"'
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
techStack: { framework: '', auth: '', database: '', dependencies: [] },
|
|
49
|
+
outputSchema: { format: 'challenges', version: 1 }
|
|
50
|
+
},
|
|
51
|
+
referee: {
|
|
52
|
+
skillDir: '/absolute/path/to/bug-hunter',
|
|
53
|
+
findings: [{ bugId: 'BUG-1', severity: '', file: '', lines: '', claim: '', evidence: '', runtimeTrigger: '' }],
|
|
54
|
+
skepticResults: { accepted: ['BUG-1'], disproved: [], details: [] },
|
|
55
|
+
outputSchema: { format: 'verdicts', version: 1 }
|
|
56
|
+
},
|
|
57
|
+
fixer: {
|
|
58
|
+
skillDir: '/absolute/path/to/bug-hunter',
|
|
59
|
+
bugs: [
|
|
60
|
+
{
|
|
61
|
+
bugId: 'BUG-1',
|
|
62
|
+
severity: 'Critical|Medium|Low',
|
|
63
|
+
file: 'src/example.ts',
|
|
64
|
+
lines: '10-15',
|
|
65
|
+
description: 'What is wrong',
|
|
66
|
+
suggestedFix: 'How to fix it'
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
techStack: { framework: '', auth: '', database: '', dependencies: [] },
|
|
70
|
+
outputSchema: { format: 'fix-report', version: 1 }
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function usage() {
|
|
75
|
+
console.error('Usage:');
|
|
76
|
+
console.error(' payload-guard.cjs validate <role> <payloadJsonPath>');
|
|
77
|
+
console.error(' payload-guard.cjs generate <role> [outputJsonPath]');
|
|
78
|
+
console.error('');
|
|
79
|
+
console.error('Roles: ' + Object.keys(REQUIRED_BY_ROLE).join(', '));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readPayload(filePath) {
|
|
83
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isNonEmptyArray(value) {
|
|
87
|
+
return Array.isArray(value) && value.length > 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function validate(role, payload) {
|
|
91
|
+
const errors = [];
|
|
92
|
+
const required = REQUIRED_BY_ROLE[role];
|
|
93
|
+
if (!required) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
errors: [`Unknown role: ${role}`]
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const field of required) {
|
|
101
|
+
if (!(field in payload)) {
|
|
102
|
+
errors.push(`Missing required field: ${field}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if ('skillDir' in payload) {
|
|
107
|
+
if (typeof payload.skillDir !== 'string' || payload.skillDir.trim() === '') {
|
|
108
|
+
errors.push('skillDir must be a non-empty string');
|
|
109
|
+
} else if (!path.isAbsolute(payload.skillDir)) {
|
|
110
|
+
errors.push('skillDir must be an absolute path');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if ('targetFiles' in payload && !isNonEmptyArray(payload.targetFiles)) {
|
|
115
|
+
errors.push('targetFiles must be a non-empty array');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if ('bugs' in payload && !isNonEmptyArray(payload.bugs)) {
|
|
119
|
+
errors.push('bugs must be a non-empty array');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if ('findings' in payload && !isNonEmptyArray(payload.findings)) {
|
|
123
|
+
errors.push('findings must be a non-empty array');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if ('skepticResults' in payload) {
|
|
127
|
+
if (!payload.skepticResults || typeof payload.skepticResults !== 'object') {
|
|
128
|
+
errors.push('skepticResults must be an object');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if ('outputSchema' in payload) {
|
|
133
|
+
if (!payload.outputSchema || typeof payload.outputSchema !== 'object') {
|
|
134
|
+
errors.push('outputSchema must be an object');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
ok: errors.length === 0,
|
|
140
|
+
errors
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function main() {
|
|
145
|
+
const [command, role, payloadJsonPath] = process.argv.slice(2);
|
|
146
|
+
|
|
147
|
+
if (command === 'generate') {
|
|
148
|
+
if (!role) {
|
|
149
|
+
usage();
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
const template = TEMPLATES[role];
|
|
153
|
+
if (!template) {
|
|
154
|
+
console.error(`Unknown role: ${role}. Available: ${Object.keys(TEMPLATES).join(', ')}`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
const output = JSON.stringify(template, null, 2);
|
|
158
|
+
if (payloadJsonPath) {
|
|
159
|
+
const fs = require('fs');
|
|
160
|
+
const path = require('path');
|
|
161
|
+
const dir = path.dirname(payloadJsonPath);
|
|
162
|
+
if (dir && !fs.existsSync(dir)) {
|
|
163
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
164
|
+
}
|
|
165
|
+
fs.writeFileSync(payloadJsonPath, output + '\n', 'utf8');
|
|
166
|
+
console.log(JSON.stringify({ ok: true, role, outputPath: payloadJsonPath }));
|
|
167
|
+
} else {
|
|
168
|
+
console.log(output);
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (command === 'validate') {
|
|
174
|
+
if (!role || !payloadJsonPath) {
|
|
175
|
+
usage();
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
const payload = readPayload(payloadJsonPath);
|
|
179
|
+
const result = validate(role, payload);
|
|
180
|
+
console.log(JSON.stringify(result, null, 2));
|
|
181
|
+
if (!result.ok) {
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
usage();
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
main();
|
|
193
|
+
} catch (error) {
|
|
194
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
195
|
+
console.error(message);
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|