@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
@@ -0,0 +1,273 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const SCHEMA_FILES = {
5
+ recon: 'recon.schema.json',
6
+ findings: 'findings.schema.json',
7
+ skeptic: 'skeptic.schema.json',
8
+ referee: 'referee.schema.json',
9
+ coverage: 'coverage.schema.json',
10
+ 'fix-report': 'fix-report.schema.json',
11
+ 'fix-plan': 'fix-plan.schema.json',
12
+ 'fix-strategy': 'fix-strategy.schema.json',
13
+ shared: 'shared.schema.json'
14
+ };
15
+
16
+ const SCHEMA_CACHE = new Map();
17
+
18
+ function getSchemaDir() {
19
+ return path.resolve(__dirname, '..', 'schemas');
20
+ }
21
+
22
+ function getKnownArtifacts() {
23
+ return Object.keys(SCHEMA_FILES).filter((name) => name !== 'shared');
24
+ }
25
+
26
+ function getSchemaPath(artifactName) {
27
+ const fileName = SCHEMA_FILES[artifactName];
28
+ if (!fileName) {
29
+ throw new Error(`Unknown artifact schema: ${artifactName}`);
30
+ }
31
+ return path.join(getSchemaDir(), fileName);
32
+ }
33
+
34
+ function loadArtifactSchema(artifactName) {
35
+ if (!SCHEMA_FILES[artifactName]) {
36
+ throw new Error(`Unknown artifact schema: ${artifactName}`);
37
+ }
38
+ if (!SCHEMA_CACHE.has(artifactName)) {
39
+ const schemaPath = getSchemaPath(artifactName);
40
+ const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8'));
41
+ if (!Number.isInteger(schema.schemaVersion) || schema.schemaVersion <= 0) {
42
+ throw new Error(`Schema ${artifactName} is missing a valid schemaVersion`);
43
+ }
44
+ SCHEMA_CACHE.set(artifactName, { schema, schemaPath });
45
+ }
46
+ return SCHEMA_CACHE.get(artifactName);
47
+ }
48
+
49
+ function createSchemaRef(artifactName) {
50
+ const { schema, schemaPath } = loadArtifactSchema(artifactName);
51
+ return {
52
+ artifact: artifactName,
53
+ schemaVersion: schema.schemaVersion,
54
+ schemaFile: path.relative(path.resolve(__dirname, '..'), schemaPath)
55
+ };
56
+ }
57
+
58
+ function validateSchemaRef(reference) {
59
+ const errors = [];
60
+ if (!reference || typeof reference !== 'object' || Array.isArray(reference)) {
61
+ return { ok: false, errors: ['outputSchema must be an object'] };
62
+ }
63
+
64
+ const artifactName = String(reference.artifact || '').trim();
65
+ if (!artifactName) {
66
+ errors.push('outputSchema.artifact must be a non-empty string');
67
+ } else if (!getKnownArtifacts().includes(artifactName)) {
68
+ errors.push(`outputSchema.artifact must be one of: ${getKnownArtifacts().join(', ')}`);
69
+ }
70
+
71
+ if (!Number.isInteger(reference.schemaVersion) || reference.schemaVersion <= 0) {
72
+ errors.push('outputSchema.schemaVersion must be a positive integer');
73
+ }
74
+
75
+ if (artifactName && getKnownArtifacts().includes(artifactName)) {
76
+ const { schema, schemaPath } = loadArtifactSchema(artifactName);
77
+ const expectedRelativePath = path.relative(path.resolve(__dirname, '..'), schemaPath);
78
+ if (reference.schemaVersion !== schema.schemaVersion) {
79
+ errors.push(`outputSchema.schemaVersion must match ${artifactName} schema version ${schema.schemaVersion}`);
80
+ }
81
+ if ('schemaFile' in reference && reference.schemaFile !== expectedRelativePath) {
82
+ errors.push(`outputSchema.schemaFile must match ${expectedRelativePath}`);
83
+ }
84
+ }
85
+
86
+ return { ok: errors.length === 0, errors };
87
+ }
88
+
89
+ function describeType(value) {
90
+ if (Array.isArray(value)) {
91
+ return 'array';
92
+ }
93
+ if (value === null) {
94
+ return 'null';
95
+ }
96
+ return typeof value;
97
+ }
98
+
99
+ function resolveRef(rootSchema, ref) {
100
+ if (!ref.startsWith('#/')) {
101
+ throw new Error(`Unsupported schema ref: ${ref}`);
102
+ }
103
+ const parts = ref
104
+ .slice(2)
105
+ .split('/')
106
+ .map((part) => part.replace(/~1/g, '/').replace(/~0/g, '~'));
107
+ let current = rootSchema;
108
+ for (const part of parts) {
109
+ if (!current || typeof current !== 'object' || !(part in current)) {
110
+ throw new Error(`Unable to resolve schema ref: ${ref}`);
111
+ }
112
+ current = current[part];
113
+ }
114
+ return current;
115
+ }
116
+
117
+ function validateAgainstSchema({ value, schema, rootSchema, jsonPath, errors }) {
118
+ if (schema.$ref) {
119
+ const resolved = resolveRef(rootSchema, schema.$ref);
120
+ validateAgainstSchema({ value, schema: resolved, rootSchema, jsonPath, errors });
121
+ return;
122
+ }
123
+
124
+ if (schema.const !== undefined && value !== schema.const) {
125
+ errors.push(`${jsonPath} must equal ${JSON.stringify(schema.const)}`);
126
+ return;
127
+ }
128
+
129
+ if (schema.enum && !schema.enum.includes(value)) {
130
+ errors.push(`${jsonPath} must be one of: ${schema.enum.join(', ')}`);
131
+ return;
132
+ }
133
+
134
+ if (schema.type === 'object') {
135
+ if (describeType(value) !== 'object') {
136
+ errors.push(`${jsonPath} must be an object`);
137
+ return;
138
+ }
139
+ const properties = schema.properties || {};
140
+ const required = schema.required || [];
141
+ for (const propertyName of required) {
142
+ if (!(propertyName in value)) {
143
+ errors.push(`${jsonPath}.${propertyName} is required`);
144
+ }
145
+ }
146
+ for (const [propertyName, propertyValue] of Object.entries(value)) {
147
+ if (properties[propertyName]) {
148
+ validateAgainstSchema({
149
+ value: propertyValue,
150
+ schema: properties[propertyName],
151
+ rootSchema,
152
+ jsonPath: `${jsonPath}.${propertyName}`,
153
+ errors
154
+ });
155
+ continue;
156
+ }
157
+ if (schema.additionalProperties === false) {
158
+ errors.push(`${jsonPath}.${propertyName} is not allowed`);
159
+ }
160
+ }
161
+ return;
162
+ }
163
+
164
+ if (schema.type === 'array') {
165
+ if (!Array.isArray(value)) {
166
+ errors.push(`${jsonPath} must be an array`);
167
+ return;
168
+ }
169
+ if (Number.isInteger(schema.minItems) && value.length < schema.minItems) {
170
+ errors.push(`${jsonPath} must contain at least ${schema.minItems} item(s)`);
171
+ }
172
+ if (schema.items) {
173
+ value.forEach((item, index) => {
174
+ validateAgainstSchema({
175
+ value: item,
176
+ schema: schema.items,
177
+ rootSchema,
178
+ jsonPath: `${jsonPath}[${index}]`,
179
+ errors
180
+ });
181
+ });
182
+ }
183
+ return;
184
+ }
185
+
186
+ if (schema.type === 'string') {
187
+ if (typeof value !== 'string') {
188
+ errors.push(`${jsonPath} must be a string`);
189
+ return;
190
+ }
191
+ if (Number.isInteger(schema.minLength) && value.length < schema.minLength) {
192
+ errors.push(`${jsonPath} must not be empty`);
193
+ }
194
+ if (schema.pattern) {
195
+ const matcher = new RegExp(schema.pattern);
196
+ if (!matcher.test(value)) {
197
+ errors.push(`${jsonPath} must match ${schema.pattern}`);
198
+ }
199
+ }
200
+ return;
201
+ }
202
+
203
+ if (schema.type === 'number') {
204
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
205
+ errors.push(`${jsonPath} must be a number`);
206
+ return;
207
+ }
208
+ if (typeof schema.minimum === 'number' && value < schema.minimum) {
209
+ errors.push(`${jsonPath} must be >= ${schema.minimum}`);
210
+ }
211
+ if (typeof schema.maximum === 'number' && value > schema.maximum) {
212
+ errors.push(`${jsonPath} must be <= ${schema.maximum}`);
213
+ }
214
+ return;
215
+ }
216
+
217
+ if (schema.type === 'integer') {
218
+ if (!Number.isInteger(value)) {
219
+ errors.push(`${jsonPath} must be an integer`);
220
+ return;
221
+ }
222
+ if (typeof schema.minimum === 'number' && value < schema.minimum) {
223
+ errors.push(`${jsonPath} must be >= ${schema.minimum}`);
224
+ }
225
+ if (typeof schema.maximum === 'number' && value > schema.maximum) {
226
+ errors.push(`${jsonPath} must be <= ${schema.maximum}`);
227
+ }
228
+ return;
229
+ }
230
+
231
+ if (schema.type === 'boolean' && typeof value !== 'boolean') {
232
+ errors.push(`${jsonPath} must be a boolean`);
233
+ }
234
+ }
235
+
236
+ function validateArtifactValue({ artifactName, value }) {
237
+ const { schema, schemaPath } = loadArtifactSchema(artifactName);
238
+ const errors = [];
239
+ validateAgainstSchema({
240
+ value,
241
+ schema,
242
+ rootSchema: schema,
243
+ jsonPath: '$',
244
+ errors
245
+ });
246
+ return {
247
+ ok: errors.length === 0,
248
+ artifact: artifactName,
249
+ schemaVersion: schema.schemaVersion,
250
+ schemaFile: path.relative(path.resolve(__dirname, '..'), schemaPath),
251
+ errors
252
+ };
253
+ }
254
+
255
+ function validateArtifactFile({ artifactName, filePath }) {
256
+ try {
257
+ const value = JSON.parse(fs.readFileSync(filePath, 'utf8'));
258
+ return validateArtifactValue({ artifactName, value });
259
+ } catch (error) {
260
+ const message = error instanceof Error ? error.message : String(error);
261
+ return { ok: false, artifact: artifactName, errors: [message] };
262
+ }
263
+ }
264
+
265
+ module.exports = {
266
+ createSchemaRef,
267
+ getKnownArtifacts,
268
+ getSchemaPath,
269
+ loadArtifactSchema,
270
+ validateArtifactFile,
271
+ validateArtifactValue,
272
+ validateSchemaRef
273
+ };
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+
3
+ const path = require('path');
4
+
5
+ const {
6
+ getKnownArtifacts,
7
+ validateArtifactFile
8
+ } = require('./schema-runtime.cjs');
9
+
10
+ function usage() {
11
+ console.error('Usage:');
12
+ console.error(' schema-validate.cjs <artifact-name> <file-path>');
13
+ console.error('');
14
+ console.error(`Artifacts: ${getKnownArtifacts().join(', ')}`);
15
+ }
16
+
17
+ function main() {
18
+ const [artifactName, targetPath] = process.argv.slice(2);
19
+ if (!artifactName || !targetPath) {
20
+ usage();
21
+ process.exit(1);
22
+ }
23
+
24
+ const result = validateArtifactFile({
25
+ artifactName,
26
+ filePath: path.resolve(targetPath)
27
+ });
28
+ console.log(JSON.stringify(result));
29
+ if (!result.ok) {
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ try {
35
+ main();
36
+ } catch (error) {
37
+ const message = error instanceof Error ? error.message : String(error);
38
+ console.error(message);
39
+ process.exit(1);
40
+ }
@@ -49,8 +49,32 @@ test('bug-hunter-state init/mark/hash/filter/record works end-to-end', () => {
49
49
 
50
50
  const findingsJson = path.join(sandbox, 'findings.json');
51
51
  writeJson(findingsJson, [
52
- { bugId: 'BUG-1', severity: 'Low', file: 'src/x.ts', lines: '1', claim: 'x' },
53
- { bugId: 'BUG-2', severity: 'Critical', file: 'src/x.ts', lines: '1', claim: 'x' }
52
+ {
53
+ bugId: 'BUG-1',
54
+ severity: 'Low',
55
+ category: 'logic',
56
+ file: 'src/x.ts',
57
+ lines: '1',
58
+ claim: 'x',
59
+ evidence: 'src/x.ts:1 first evidence',
60
+ runtimeTrigger: 'Call x()',
61
+ crossReferences: ['Single file'],
62
+ confidenceScore: 40
63
+ },
64
+ {
65
+ bugId: 'BUG-2',
66
+ severity: 'Critical',
67
+ category: 'security',
68
+ file: 'src/x.ts',
69
+ lines: '1',
70
+ claim: 'x',
71
+ evidence: 'src/x.ts:1 upgraded evidence',
72
+ runtimeTrigger: 'Call x() with attacker-controlled input',
73
+ crossReferences: ['Single file'],
74
+ confidenceScore: 95,
75
+ stride: 'Tampering',
76
+ cwe: 'CWE-20'
77
+ }
54
78
  ]);
55
79
  const recorded = runJson('node', [stateScript, 'record-findings', statePath, findingsJson, 'test']);
56
80
  assert.equal(recorded.inserted, 1);
@@ -64,7 +88,8 @@ test('bug-hunter-state init/mark/hash/filter/record works end-to-end', () => {
64
88
 
65
89
  const state = readJson(statePath);
66
90
  assert.equal(state.bugLedger[0].severity, 'Critical');
67
- assert.equal(state.metrics.lowConfidenceFindings, 1);
91
+ assert.equal(state.bugLedger[0].confidenceScore, 95);
92
+ assert.equal(state.metrics.lowConfidenceFindings, 0);
68
93
 
69
94
  const extraFile = path.join(sandbox, 'c.ts');
70
95
  fs.writeFileSync(extraFile, 'const c = 3;\n', 'utf8');
@@ -85,3 +110,43 @@ test('bug-hunter-state init/mark/hash/filter/record works end-to-end', () => {
85
110
  const updatedState = readJson(statePath);
86
111
  assert.equal(updatedState.factCards['chunk-1'].apiContracts.length, 1);
87
112
  });
113
+
114
+ test('bug-hunter-state rejects malformed findings artifacts', () => {
115
+ const sandbox = makeSandbox('bug-hunter-state-invalid-');
116
+ const stateScript = resolveSkillScript('bug-hunter-state.cjs');
117
+ const filePath = path.join(sandbox, 'a.ts');
118
+ fs.writeFileSync(filePath, 'const a = 1;\n', 'utf8');
119
+
120
+ const filesJson = path.join(sandbox, 'files.json');
121
+ writeJson(filesJson, [filePath]);
122
+ const statePath = path.join(sandbox, 'state.json');
123
+ runJson('node', [stateScript, 'init', statePath, 'extended', filesJson, '1']);
124
+
125
+ const findingsJson = path.join(sandbox, 'findings.json');
126
+ writeJson(findingsJson, [
127
+ {
128
+ bugId: 'BUG-1',
129
+ severity: 'Low',
130
+ category: 'logic',
131
+ file: 'src/x.ts',
132
+ lines: '1',
133
+ evidence: 'src/x.ts:1 evidence',
134
+ runtimeTrigger: 'Call x()',
135
+ crossReferences: ['Single file'],
136
+ confidenceScore: 40
137
+ }
138
+ ]);
139
+
140
+ const result = require('node:child_process').spawnSync('node', [
141
+ stateScript,
142
+ 'record-findings',
143
+ statePath,
144
+ findingsJson,
145
+ 'test'
146
+ ], {
147
+ encoding: 'utf8'
148
+ });
149
+
150
+ assert.notEqual(result.status, 0);
151
+ assert.match(`${result.stderr}${result.stdout}`, /Invalid findings artifact/);
152
+ });
@@ -55,3 +55,18 @@ test('code-index build captures symbols, call graph, boundaries, and query scope
55
55
  assert.equal(queryResult.selected.includes(serviceFile), true);
56
56
  assert.equal(queryResult.trustBoundaryFiles.includes(routeFile), true);
57
57
  });
58
+
59
+ test('code-index query-bugs cleans up temp seed files after failures', () => {
60
+ const sandbox = makeSandbox('code-index-query-bugs-');
61
+ const codeIndex = resolveSkillScript('code-index.cjs');
62
+ const bugsJson = path.join(sandbox, 'bugs.json');
63
+ const missingIndexPath = path.join(sandbox, 'missing-index.json');
64
+ const sourceFile = path.join(sandbox, 'src', 'feature.ts');
65
+ fs.mkdirSync(path.dirname(sourceFile), { recursive: true });
66
+ fs.writeFileSync(sourceFile, 'export const feature = true;\n', 'utf8');
67
+ writeJson(bugsJson, [{ bugId: 'BUG-1', file: sourceFile }]);
68
+
69
+ const result = require('./test-utils.cjs').runRaw('node', [codeIndex, 'query-bugs', missingIndexPath, bugsJson, '1']);
70
+ assert.notEqual(result.status, 0);
71
+ assert.equal(fs.existsSync(path.join(sandbox, '.seed-files.tmp.json')), false);
72
+ });
@@ -9,7 +9,7 @@ const {
9
9
  runRaw
10
10
  } = require('./test-utils.cjs');
11
11
 
12
- test('fix-lock enforces single writer and supports release', () => {
12
+ test('fix-lock enforces single writer and supports token-protected release', () => {
13
13
  const sandbox = makeSandbox('fix-lock-');
14
14
  const lockScript = resolveSkillScript('fix-lock.cjs');
15
15
  const lockPath = path.join(sandbox, 'bug-hunter-fix.lock');
@@ -17,20 +17,78 @@ test('fix-lock enforces single writer and supports release', () => {
17
17
  const acquire1 = runJson('node', [lockScript, 'acquire', lockPath, '120']);
18
18
  assert.equal(acquire1.ok, true);
19
19
  assert.equal(acquire1.acquired, true);
20
+ assert.equal(typeof acquire1.lock.ownerToken, 'string');
21
+ assert.equal(acquire1.lock.ownerToken.length > 8, true);
20
22
 
21
23
  const acquire2 = runRaw('node', [lockScript, 'acquire', lockPath, '120']);
22
24
  assert.notEqual(acquire2.status, 0);
23
25
  const output2 = `${acquire2.stdout || ''}${acquire2.stderr || ''}`;
24
26
  assert.match(output2, /lock-held/);
25
27
 
28
+ const renew = runJson('node', [lockScript, 'renew', lockPath, acquire1.lock.ownerToken]);
29
+ assert.equal(renew.ok, true);
30
+ assert.equal(renew.renewed, true);
31
+
32
+ const badRelease = runRaw('node', [lockScript, 'release', lockPath, 'wrong-token']);
33
+ assert.notEqual(badRelease.status, 0);
34
+ assert.match(`${badRelease.stdout || ''}${badRelease.stderr || ''}`, /lock-owner-mismatch/);
35
+
26
36
  const status = runJson('node', [lockScript, 'status', lockPath, '120']);
27
37
  assert.equal(status.exists, true);
28
38
  assert.equal(status.stale, false);
29
39
 
30
- const release = runJson('node', [lockScript, 'release', lockPath]);
40
+ const release = runJson('node', [lockScript, 'release', lockPath, acquire1.lock.ownerToken]);
31
41
  assert.equal(release.ok, true);
32
42
  assert.equal(release.released, true);
33
43
 
34
44
  const statusAfter = runJson('node', [lockScript, 'status', lockPath, '120']);
35
45
  assert.equal(statusAfter.exists, false);
36
46
  });
47
+
48
+ test('fix-lock does not steal an expired lock from a still-running owner', () => {
49
+ const sandbox = makeSandbox('fix-lock-live-owner-');
50
+ const lockScript = resolveSkillScript('fix-lock.cjs');
51
+ const lockPath = path.join(sandbox, 'bug-hunter-fix.lock');
52
+
53
+ require('fs').writeFileSync(lockPath, `${JSON.stringify({
54
+ pid: process.pid,
55
+ host: 'test-host',
56
+ cwd: sandbox,
57
+ createdAtMs: Date.now() - 10_000,
58
+ createdAt: new Date(Date.now() - 10_000).toISOString(),
59
+ ownerToken: 'existing-owner-token'
60
+ }, null, 2)}\n`, 'utf8');
61
+
62
+ const acquire = runRaw('node', [lockScript, 'acquire', lockPath, '1']);
63
+ assert.notEqual(acquire.status, 0);
64
+ assert.match(`${acquire.stdout || ''}${acquire.stderr || ''}`, /lock-held-by-live-owner|lock-held/);
65
+ });
66
+
67
+ test('fix-lock acquires atomically under contention', async () => {
68
+ const sandbox = makeSandbox('fix-lock-race-');
69
+ const lockScript = resolveSkillScript('fix-lock.cjs');
70
+ const lockPath = path.join(sandbox, 'bug-hunter-fix.lock');
71
+
72
+ const results = await Promise.all(Array.from({ length: 20 }, () => {
73
+ return new Promise((resolve) => {
74
+ const child = require('node:child_process').spawn('node', [lockScript, 'acquire', lockPath, '120'], {
75
+ stdio: ['ignore', 'pipe', 'pipe']
76
+ });
77
+ child.on('close', (code) => resolve(code));
78
+ });
79
+ }));
80
+
81
+ const successCount = results.filter((code) => code === 0).length;
82
+ assert.equal(successCount, 1);
83
+ });
84
+
85
+ test('fix-lock recovers from a corrupted lock file', () => {
86
+ const sandbox = makeSandbox('fix-lock-corrupt-');
87
+ const lockScript = resolveSkillScript('fix-lock.cjs');
88
+ const lockPath = path.join(sandbox, 'bug-hunter-fix.lock');
89
+ require('fs').writeFileSync(lockPath, '{broken json', 'utf8');
90
+
91
+ const result = runJson('node', [lockScript, 'acquire', lockPath, '120']);
92
+ assert.equal(result.ok, true);
93
+ assert.equal(result.acquired, true);
94
+ });
@@ -54,9 +54,14 @@ if (findingsJson) {
54
54
  {
55
55
  bugId: `BUG-${chunkId}`,
56
56
  severity: 'Medium',
57
+ category: 'logic',
57
58
  file: `src/retry-${chunkId}.ts`,
58
59
  lines: '10-11',
59
- claim: `retry-success-${chunkId}`
60
+ claim: `retry-success-${chunkId}`,
61
+ evidence: `src/retry-${chunkId}.ts:10-11 retry success evidence`,
62
+ runtimeTrigger: `Retry attempt for ${chunkId}`,
63
+ crossReferences: ['Single file'],
64
+ confidenceScore: 88
60
65
  }
61
66
  ];
62
67
  fs.writeFileSync(findingsJson, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
@@ -54,10 +54,16 @@ if (findingsJson && scanFiles.length > 0) {
54
54
  {
55
55
  bugId: `BUG-${chunkId}`,
56
56
  severity: 'Critical',
57
- confidence: Number.isInteger(confidence) ? confidence : 60,
57
+ category: 'security',
58
58
  file: scanFiles[0],
59
59
  lines: '1',
60
- claim: `Low-confidence risk in ${path.basename(scanFiles[0])}`
60
+ claim: `Low-confidence risk in ${path.basename(scanFiles[0])}`,
61
+ evidence: `${scanFiles[0]}:1 fixture evidence`,
62
+ runtimeTrigger: `Load ${path.basename(scanFiles[0])} through the low-confidence worker`,
63
+ crossReferences: ['Single file'],
64
+ confidenceScore: Number.isInteger(confidence) ? confidence : 60,
65
+ stride: 'Tampering',
66
+ cwe: 'CWE-20'
61
67
  }
62
68
  ]);
63
69
  }
@@ -34,9 +34,14 @@ const payload = [
34
34
  {
35
35
  bugId: `BUG-${options['chunk-id'] || '0'}`,
36
36
  severity: 'Low',
37
+ category: 'logic',
37
38
  file: 'src/example.ts',
38
39
  lines: '1',
39
- claim: 'example'
40
+ claim: 'example',
41
+ evidence: 'src/example.ts:1 example evidence',
42
+ runtimeTrigger: 'Run the success worker fixture',
43
+ crossReferences: ['Single file'],
44
+ confidenceScore: 80
40
45
  }
41
46
  ];
42
47
  fs.writeFileSync(findingsJson, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');