@codexstar/bug-hunter 3.0.0 → 3.0.5

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 (77) hide show
  1. package/CHANGELOG.md +149 -83
  2. package/README.md +150 -15
  3. package/SKILL.md +94 -27
  4. package/agents/openai.yaml +4 -0
  5. package/bin/bug-hunter +9 -3
  6. package/docs/images/2026-03-12-fix-plan-rollout.png +0 -0
  7. package/docs/images/2026-03-12-hero-bug-hunter-overview.png +0 -0
  8. package/docs/images/2026-03-12-machine-readable-artifacts.png +0 -0
  9. package/docs/images/2026-03-12-pr-review-flow.png +0 -0
  10. package/docs/images/2026-03-12-security-pack.png +0 -0
  11. package/docs/images/adversarial-debate.png +0 -0
  12. package/docs/images/doc-verify-fix-plan.png +0 -0
  13. package/docs/images/hero.png +0 -0
  14. package/docs/images/pipeline-overview.png +0 -0
  15. package/docs/images/security-finding-card.png +0 -0
  16. package/docs/plans/2026-03-11-structured-output-migration-plan.md +288 -0
  17. package/docs/plans/2026-03-12-audit-bug-fixes-surgical-plan.md +193 -0
  18. package/docs/plans/2026-03-12-enterprise-security-pack-e2e-plan.md +59 -0
  19. package/docs/plans/2026-03-12-local-security-skills-integration-plan.md +39 -0
  20. package/docs/plans/2026-03-12-pr-review-strategic-fix-flow.md +78 -0
  21. package/evals/evals.json +366 -102
  22. package/modes/extended.md +2 -2
  23. package/modes/fix-loop.md +30 -30
  24. package/modes/fix-pipeline.md +32 -6
  25. package/modes/large-codebase.md +14 -15
  26. package/modes/local-sequential.md +44 -20
  27. package/modes/loop.md +56 -56
  28. package/modes/parallel.md +3 -3
  29. package/modes/scaled.md +2 -2
  30. package/modes/single-file.md +3 -3
  31. package/modes/small.md +11 -11
  32. package/package.json +10 -1
  33. package/prompts/fixer.md +37 -23
  34. package/prompts/hunter.md +39 -20
  35. package/prompts/referee.md +34 -20
  36. package/prompts/skeptic.md +25 -22
  37. package/schemas/coverage.schema.json +67 -0
  38. package/schemas/examples/findings.invalid.json +13 -0
  39. package/schemas/examples/findings.valid.json +17 -0
  40. package/schemas/findings.schema.json +76 -0
  41. package/schemas/fix-plan.schema.json +94 -0
  42. package/schemas/fix-report.schema.json +105 -0
  43. package/schemas/fix-strategy.schema.json +99 -0
  44. package/schemas/recon.schema.json +31 -0
  45. package/schemas/referee.schema.json +46 -0
  46. package/schemas/shared.schema.json +51 -0
  47. package/schemas/skeptic.schema.json +21 -0
  48. package/scripts/bug-hunter-state.cjs +35 -12
  49. package/scripts/code-index.cjs +11 -4
  50. package/scripts/fix-lock.cjs +95 -25
  51. package/scripts/payload-guard.cjs +24 -10
  52. package/scripts/pr-scope.cjs +181 -0
  53. package/scripts/render-report.cjs +346 -0
  54. package/scripts/run-bug-hunter.cjs +667 -32
  55. package/scripts/schema-runtime.cjs +273 -0
  56. package/scripts/schema-validate.cjs +40 -0
  57. package/scripts/tests/bug-hunter-state.test.cjs +68 -3
  58. package/scripts/tests/code-index.test.cjs +15 -0
  59. package/scripts/tests/fix-lock.test.cjs +60 -2
  60. package/scripts/tests/fixtures/flaky-worker.cjs +6 -1
  61. package/scripts/tests/fixtures/low-confidence-worker.cjs +8 -2
  62. package/scripts/tests/fixtures/success-worker.cjs +6 -1
  63. package/scripts/tests/payload-guard.test.cjs +154 -2
  64. package/scripts/tests/pr-scope.test.cjs +212 -0
  65. package/scripts/tests/render-report.test.cjs +180 -0
  66. package/scripts/tests/run-bug-hunter.test.cjs +686 -2
  67. package/scripts/tests/security-skills-integration.test.cjs +29 -0
  68. package/scripts/tests/skills-packaging.test.cjs +30 -0
  69. package/scripts/tests/worktree-harvest.test.cjs +66 -0
  70. package/scripts/worktree-harvest.cjs +62 -9
  71. package/skills/README.md +19 -0
  72. package/skills/commit-security-scan/SKILL.md +63 -0
  73. package/skills/security-review/SKILL.md +57 -0
  74. package/skills/threat-model-generation/SKILL.md +47 -0
  75. package/skills/vulnerability-validation/SKILL.md +59 -0
  76. package/templates/subagent-wrapper.md +12 -3
  77. package/modes/_dispatch.md +0 -121
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const crypto = require('crypto');
3
4
  const fs = require('fs');
4
5
  const os = require('os');
5
6
  const path = require('path');
@@ -7,9 +8,10 @@ const path = require('path');
7
8
  function usage() {
8
9
  console.error('Usage:');
9
10
  console.error(' fix-lock.cjs acquire <lockPath> [ttlSeconds]');
10
- console.error(' fix-lock.cjs renew <lockPath>');
11
- console.error(' fix-lock.cjs release <lockPath>');
11
+ console.error(' fix-lock.cjs renew <lockPath> <ownerToken>');
12
+ console.error(' fix-lock.cjs release <lockPath> <ownerToken>');
12
13
  console.error(' fix-lock.cjs status <lockPath> [ttlSeconds]');
14
+ console.error(' Note: acquire returns lock.ownerToken; pass it to renew/release.');
13
15
  }
14
16
 
15
17
  function nowMs() {
@@ -24,7 +26,11 @@ function readLock(lockPath) {
24
26
  if (!fs.existsSync(lockPath)) {
25
27
  return null;
26
28
  }
27
- return JSON.parse(fs.readFileSync(lockPath, 'utf8'));
29
+ try {
30
+ return JSON.parse(fs.readFileSync(lockPath, 'utf8'));
31
+ } catch {
32
+ return null;
33
+ }
28
34
  }
29
35
 
30
36
  function pidAlive(pid) {
@@ -47,67 +53,131 @@ function lockIsStale(lockData, ttlSeconds) {
47
53
  return expired;
48
54
  }
49
55
 
50
- function writeLock(lockPath) {
56
+ function writeLock(lockPath, ownerTokenRaw, exclusive = true) {
51
57
  ensureParent(lockPath);
52
58
  const lockData = {
53
59
  pid: process.pid,
54
60
  host: os.hostname(),
55
61
  cwd: process.cwd(),
62
+ ownerToken: ownerTokenRaw || crypto.randomUUID(),
56
63
  createdAtMs: nowMs(),
57
64
  createdAt: new Date().toISOString()
58
65
  };
59
- fs.writeFileSync(lockPath, `${JSON.stringify(lockData, null, 2)}\n`, 'utf8');
66
+ const fd = fs.openSync(lockPath, exclusive ? 'wx' : 'w');
67
+ try {
68
+ fs.writeFileSync(fd, `${JSON.stringify(lockData, null, 2)}\n`, 'utf8');
69
+ } finally {
70
+ fs.closeSync(fd);
71
+ }
60
72
  return lockData;
61
73
  }
62
74
 
63
- function renew(lockPath) {
75
+ function assertOwner(existing, ownerToken) {
76
+ if (!existing || !existing.ownerToken) {
77
+ return true;
78
+ }
79
+ return ownerToken === existing.ownerToken;
80
+ }
81
+
82
+ function renew(lockPath, ownerToken) {
64
83
  const existing = readLock(lockPath);
65
84
  if (!existing) {
66
85
  console.log(JSON.stringify({ ok: false, renewed: false, reason: 'no-lock' }, null, 2));
67
86
  process.exit(1);
68
87
  return;
69
88
  }
89
+ if (!assertOwner(existing, ownerToken)) {
90
+ console.log(JSON.stringify({ ok: false, renewed: false, reason: 'lock-owner-mismatch' }, null, 2));
91
+ process.exit(1);
92
+ return;
93
+ }
70
94
  existing.createdAtMs = nowMs();
71
95
  existing.renewedAt = new Date().toISOString();
72
- fs.writeFileSync(lockPath, `${JSON.stringify(existing, null, 2)}\n`, 'utf8');
96
+ const tempPath = `${lockPath}.${process.pid}.tmp`;
97
+ fs.writeFileSync(tempPath, `${JSON.stringify(existing, null, 2)}\n`, 'utf8');
98
+ fs.renameSync(tempPath, lockPath);
73
99
  console.log(JSON.stringify({ ok: true, renewed: true, lock: existing }, null, 2));
74
100
  }
75
101
 
76
102
  function acquire(lockPath, ttlSeconds) {
77
103
  const existing = readLock(lockPath);
78
104
  if (!existing) {
79
- const lockData = writeLock(lockPath);
80
- console.log(JSON.stringify({ ok: true, acquired: true, lock: lockData }, null, 2));
81
- return;
105
+ if (fs.existsSync(lockPath)) {
106
+ fs.unlinkSync(lockPath);
107
+ }
108
+ try {
109
+ const lockData = writeLock(lockPath);
110
+ console.log(JSON.stringify({ ok: true, acquired: true, lock: lockData }, null, 2));
111
+ return;
112
+ } catch (error) {
113
+ if (error && error.code === 'EEXIST') {
114
+ const current = readLock(lockPath);
115
+ console.log(JSON.stringify({
116
+ ok: false,
117
+ acquired: false,
118
+ reason: 'lock-held',
119
+ lock: current
120
+ }, null, 2));
121
+ process.exit(1);
122
+ return;
123
+ }
124
+ throw error;
125
+ }
82
126
  }
83
127
 
84
- if (!lockIsStale(existing, ttlSeconds)) {
128
+ const stale = lockIsStale(existing, ttlSeconds);
129
+ const ownerAlive = typeof existing.pid === 'number' ? pidAlive(existing.pid) : false;
130
+
131
+ if (!stale || ownerAlive) {
85
132
  console.log(JSON.stringify({
86
133
  ok: false,
87
134
  acquired: false,
88
- reason: 'lock-held',
135
+ reason: ownerAlive ? 'lock-held-by-live-owner' : 'lock-held',
136
+ stale,
137
+ ownerAlive,
89
138
  lock: existing
90
139
  }, null, 2));
91
140
  process.exit(1);
92
141
  }
93
142
 
94
143
  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));
144
+ try {
145
+ const lockData = writeLock(lockPath);
146
+ console.log(JSON.stringify({
147
+ ok: true,
148
+ acquired: true,
149
+ recoveredFromStaleLock: true,
150
+ previousLock: existing,
151
+ lock: lockData
152
+ }, null, 2));
153
+ return;
154
+ } catch (error) {
155
+ if (error && error.code === 'EEXIST') {
156
+ const current = readLock(lockPath);
157
+ console.log(JSON.stringify({
158
+ ok: false,
159
+ acquired: false,
160
+ reason: 'lock-held',
161
+ lock: current
162
+ }, null, 2));
163
+ process.exit(1);
164
+ return;
165
+ }
166
+ throw error;
167
+ }
103
168
  }
104
169
 
105
- function release(lockPath) {
170
+ function release(lockPath, ownerToken) {
106
171
  const existing = readLock(lockPath);
107
172
  if (!existing) {
108
173
  console.log(JSON.stringify({ ok: true, released: false, reason: 'no-lock' }, null, 2));
109
174
  return;
110
175
  }
176
+ if (!assertOwner(existing, ownerToken)) {
177
+ console.log(JSON.stringify({ ok: false, released: false, reason: 'lock-owner-mismatch' }, null, 2));
178
+ process.exit(1);
179
+ return;
180
+ }
111
181
  fs.unlinkSync(lockPath);
112
182
  console.log(JSON.stringify({ ok: true, released: true, previousLock: existing }, null, 2));
113
183
  }
@@ -130,12 +200,12 @@ function status(lockPath, ttlSeconds) {
130
200
  }
131
201
 
132
202
  function main() {
133
- const [command, lockPath, ttlRaw] = process.argv.slice(2);
203
+ const [command, lockPath, rawArg] = process.argv.slice(2);
134
204
  if (!command || !lockPath) {
135
205
  usage();
136
206
  process.exit(1);
137
207
  }
138
- const ttlParsed = Number.parseInt(ttlRaw || '', 10);
208
+ const ttlParsed = Number.parseInt(rawArg || '', 10);
139
209
  const ttlSeconds = Number.isInteger(ttlParsed) && ttlParsed > 0 ? ttlParsed : 1800;
140
210
 
141
211
  if (command === 'acquire') {
@@ -143,11 +213,11 @@ function main() {
143
213
  return;
144
214
  }
145
215
  if (command === 'renew') {
146
- renew(lockPath);
216
+ renew(lockPath, rawArg);
147
217
  return;
148
218
  }
149
219
  if (command === 'release') {
150
- release(lockPath);
220
+ release(lockPath, rawArg);
151
221
  return;
152
222
  }
153
223
  if (command === 'status') {
@@ -2,6 +2,10 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const {
6
+ createSchemaRef,
7
+ validateSchemaRef
8
+ } = require('./schema-runtime.cjs');
5
9
 
6
10
  const REQUIRED_BY_ROLE = {
7
11
  recon: ['skillDir', 'targetFiles', 'outputSchema'],
@@ -16,20 +20,20 @@ const TEMPLATES = {
16
20
  recon: {
17
21
  skillDir: '/absolute/path/to/bug-hunter',
18
22
  targetFiles: ['src/example.ts'],
19
- outputSchema: { format: 'risk-map', version: 1 }
23
+ outputSchema: createSchemaRef('recon')
20
24
  },
21
25
  'triage-hunter': {
22
26
  skillDir: '/absolute/path/to/bug-hunter',
23
27
  targetFiles: ['src/example.ts'],
24
28
  techStack: { framework: '', auth: '', database: '', dependencies: [] },
25
- outputSchema: { format: 'triage-findings', version: 1 }
29
+ outputSchema: createSchemaRef('findings')
26
30
  },
27
31
  hunter: {
28
32
  skillDir: '/absolute/path/to/bug-hunter',
29
33
  targetFiles: ['src/example.ts'],
30
34
  riskMap: { critical: [], high: [], medium: [], contextOnly: [] },
31
35
  techStack: { framework: '', auth: '', database: '', dependencies: [] },
32
- outputSchema: { format: 'findings', version: 1 }
36
+ outputSchema: createSchemaRef('findings')
33
37
  },
34
38
  skeptic: {
35
39
  skillDir: '/absolute/path/to/bug-hunter',
@@ -46,13 +50,24 @@ const TEMPLATES = {
46
50
  }
47
51
  ],
48
52
  techStack: { framework: '', auth: '', database: '', dependencies: [] },
49
- outputSchema: { format: 'challenges', version: 1 }
53
+ outputSchema: createSchemaRef('skeptic')
50
54
  },
51
55
  referee: {
52
56
  skillDir: '/absolute/path/to/bug-hunter',
53
- findings: [{ bugId: 'BUG-1', severity: '', file: '', lines: '', claim: '', evidence: '', runtimeTrigger: '' }],
57
+ findings: [{
58
+ bugId: 'BUG-1',
59
+ severity: 'Critical',
60
+ category: 'security',
61
+ file: 'src/example.ts',
62
+ lines: '10-15',
63
+ claim: 'One-sentence description of the bug',
64
+ evidence: 'Exact code quote from the file',
65
+ runtimeTrigger: 'Specific scenario that triggers this bug',
66
+ crossReferences: ['Single file'],
67
+ confidenceScore: 92
68
+ }],
54
69
  skepticResults: { accepted: ['BUG-1'], disproved: [], details: [] },
55
- outputSchema: { format: 'verdicts', version: 1 }
70
+ outputSchema: createSchemaRef('referee')
56
71
  },
57
72
  fixer: {
58
73
  skillDir: '/absolute/path/to/bug-hunter',
@@ -67,7 +82,7 @@ const TEMPLATES = {
67
82
  }
68
83
  ],
69
84
  techStack: { framework: '', auth: '', database: '', dependencies: [] },
70
- outputSchema: { format: 'fix-report', version: 1 }
85
+ outputSchema: createSchemaRef('fix-report')
71
86
  }
72
87
  };
73
88
 
@@ -130,9 +145,8 @@ function validate(role, payload) {
130
145
  }
131
146
 
132
147
  if ('outputSchema' in payload) {
133
- if (!payload.outputSchema || typeof payload.outputSchema !== 'object') {
134
- errors.push('outputSchema must be an object');
135
- }
148
+ const schemaValidation = validateSchemaRef(payload.outputSchema);
149
+ errors.push(...schemaValidation.errors);
136
150
  }
137
151
 
138
152
  return {
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+
3
+ const childProcess = require('child_process');
4
+ const path = require('path');
5
+
6
+ function usage() {
7
+ console.error('Usage:');
8
+ console.error(' pr-scope.cjs resolve <current|recent|pr-number> [--repo-root <path>] [--base <branch>] [--gh-bin <path>] [--git-bin <path>]');
9
+ }
10
+
11
+ function parseOptions(argv) {
12
+ const options = {};
13
+ let index = 0;
14
+ while (index < argv.length) {
15
+ const token = argv[index];
16
+ if (!token.startsWith('--')) {
17
+ index += 1;
18
+ continue;
19
+ }
20
+ const key = token.slice(2);
21
+ const value = argv[index + 1];
22
+ if (!value || value.startsWith('--')) {
23
+ options[key] = 'true';
24
+ index += 1;
25
+ continue;
26
+ }
27
+ options[key] = value;
28
+ index += 2;
29
+ }
30
+ return options;
31
+ }
32
+
33
+ function runJson(bin, args, cwd) {
34
+ const result = childProcess.spawnSync(bin, args, {
35
+ encoding: 'utf8',
36
+ cwd
37
+ });
38
+ if (result.status !== 0) {
39
+ const stderr = (result.stderr || '').trim();
40
+ const stdout = (result.stdout || '').trim();
41
+ throw new Error(stderr || stdout || `${bin} ${args.join(' ')} failed`);
42
+ }
43
+ const output = (result.stdout || '').trim();
44
+ return output ? JSON.parse(output) : null;
45
+ }
46
+
47
+ function runLines(bin, args, cwd) {
48
+ const result = childProcess.spawnSync(bin, args, {
49
+ encoding: 'utf8',
50
+ cwd
51
+ });
52
+ if (result.status !== 0) {
53
+ const stderr = (result.stderr || '').trim();
54
+ const stdout = (result.stdout || '').trim();
55
+ throw new Error(stderr || stdout || `${bin} ${args.join(' ')} failed`);
56
+ }
57
+ return (result.stdout || '')
58
+ .split(/\r?\n/)
59
+ .map((line) => line.trim())
60
+ .filter(Boolean);
61
+ }
62
+
63
+ function ghMetadataSelector(selector) {
64
+ if (selector === 'current') {
65
+ return [];
66
+ }
67
+ return [String(selector)];
68
+ }
69
+
70
+ function resolveWithGh({ selector, ghBin, cwd }) {
71
+ if (selector === 'recent') {
72
+ const list = runJson(ghBin, [
73
+ 'pr',
74
+ 'list',
75
+ '--limit',
76
+ '1',
77
+ '--state',
78
+ 'open',
79
+ '--json',
80
+ 'number,title,headRefName,baseRefName,url'
81
+ ], cwd);
82
+ const pr = Array.isArray(list) ? list[0] : null;
83
+ if (!pr) {
84
+ throw new Error('No recent pull requests found');
85
+ }
86
+ const changedFiles = runLines(ghBin, ['pr', 'diff', String(pr.number), '--name-only'], cwd);
87
+ return {
88
+ ok: true,
89
+ source: 'gh',
90
+ selector,
91
+ pr,
92
+ changedFiles
93
+ };
94
+ }
95
+
96
+ const pr = runJson(ghBin, [
97
+ 'pr',
98
+ 'view',
99
+ ...ghMetadataSelector(selector),
100
+ '--json',
101
+ 'number,title,headRefName,baseRefName,url'
102
+ ], cwd);
103
+ const changedFiles = runLines(ghBin, ['pr', 'diff', ...ghMetadataSelector(selector), '--name-only'], cwd);
104
+ return {
105
+ ok: true,
106
+ source: 'gh',
107
+ selector,
108
+ pr,
109
+ changedFiles
110
+ };
111
+ }
112
+
113
+ function resolveDefaultBaseBranch({ gitBin, cwd, explicitBase }) {
114
+ if (explicitBase) {
115
+ return { baseRefName: explicitBase, diffBaseRef: explicitBase };
116
+ }
117
+
118
+ const symbolicRef = runLines(gitBin, ['symbolic-ref', 'refs/remotes/origin/HEAD'], cwd)[0];
119
+ const match = symbolicRef && symbolicRef.match(/^refs\/remotes\/origin\/(.+)$/);
120
+ if (match && match[1]) {
121
+ return { baseRefName: match[1], diffBaseRef: `origin/${match[1]}` };
122
+ }
123
+
124
+ throw new Error('Unable to determine default base branch for git fallback');
125
+ }
126
+
127
+ function resolveWithGitFallback({ gitBin, cwd, base }) {
128
+ const headRefName = runLines(gitBin, ['rev-parse', '--abbrev-ref', 'HEAD'], cwd)[0];
129
+ const { baseRefName, diffBaseRef } = resolveDefaultBaseBranch({ gitBin, cwd, explicitBase: base });
130
+ const changedFiles = runLines(gitBin, ['diff', '--name-only', `${diffBaseRef}...${headRefName}`], cwd);
131
+ return {
132
+ ok: true,
133
+ source: 'git',
134
+ selector: 'current',
135
+ pr: {
136
+ number: null,
137
+ title: `Current branch diff (${headRefName} vs ${baseRefName})`,
138
+ headRefName,
139
+ baseRefName,
140
+ url: null
141
+ },
142
+ changedFiles
143
+ };
144
+ }
145
+
146
+ function resolveScope({ selector, options }) {
147
+ const cwd = path.resolve(options['repo-root'] || process.cwd());
148
+ const ghBin = options['gh-bin'] || process.env.BUG_HUNTER_GH_BIN || 'gh';
149
+ const gitBin = options['git-bin'] || process.env.BUG_HUNTER_GIT_BIN || 'git';
150
+ const base = options.base || null;
151
+
152
+ try {
153
+ return resolveWithGh({ selector, ghBin, cwd });
154
+ } catch (error) {
155
+ if (selector !== 'current') {
156
+ throw error;
157
+ }
158
+ const fallback = resolveWithGitFallback({ gitBin, cwd, base });
159
+ fallback.fallbackReason = error instanceof Error ? error.message : String(error);
160
+ return fallback;
161
+ }
162
+ }
163
+
164
+ function main() {
165
+ const [command, selector, ...rest] = process.argv.slice(2);
166
+ if (command !== 'resolve' || !selector) {
167
+ usage();
168
+ process.exit(1);
169
+ }
170
+ const options = parseOptions(rest);
171
+ const result = resolveScope({ selector, options });
172
+ console.log(JSON.stringify(result, null, 2));
173
+ }
174
+
175
+ try {
176
+ main();
177
+ } catch (error) {
178
+ const message = error instanceof Error ? error.message : String(error);
179
+ console.error(message);
180
+ process.exit(1);
181
+ }