@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
@@ -13,6 +13,7 @@ const {
13
13
  test('payload-guard accepts valid hunter payload and rejects malformed payload', () => {
14
14
  const sandbox = makeSandbox('payload-guard-');
15
15
  const guardScript = resolveSkillScript('payload-guard.cjs');
16
+ const schemaRuntime = require(resolveSkillScript('schema-runtime.cjs'));
16
17
  const validPayloadPath = path.join(sandbox, 'valid.json');
17
18
  const invalidPayloadPath = path.join(sandbox, 'invalid.json');
18
19
 
@@ -21,7 +22,7 @@ test('payload-guard accepts valid hunter payload and rejects malformed payload',
21
22
  targetFiles: ['src/a.ts'],
22
23
  riskMap: {},
23
24
  techStack: { framework: 'express' },
24
- outputSchema: { type: 'object' }
25
+ outputSchema: schemaRuntime.createSchemaRef('findings')
25
26
  });
26
27
 
27
28
  const valid = runJson('node', [guardScript, 'validate', 'hunter', validPayloadPath]);
@@ -31,11 +32,162 @@ test('payload-guard accepts valid hunter payload and rejects malformed payload',
31
32
  writeJson(invalidPayloadPath, {
32
33
  skillDir: 'relative/path',
33
34
  targetFiles: [],
34
- outputSchema: null
35
+ outputSchema: { artifact: 'findings', schemaVersion: 999, schemaFile: 'schemas/findings.schema.json' }
35
36
  });
36
37
 
37
38
  const invalid = runRaw('node', [guardScript, 'validate', 'hunter', invalidPayloadPath]);
38
39
  assert.notEqual(invalid.status, 0);
39
40
  const output = `${invalid.stdout || ''}\n${invalid.stderr || ''}`;
40
41
  assert.match(output, /Missing required field: riskMap/);
42
+ assert.match(output, /schema version 1/);
43
+ });
44
+
45
+ test('schema-validate validates example findings fixtures', () => {
46
+ const validatorScript = resolveSkillScript('schema-validate.cjs');
47
+ const validPath = resolveSkillScript('..', 'schemas', 'examples', 'findings.valid.json');
48
+ const invalidPath = resolveSkillScript('..', 'schemas', 'examples', 'findings.invalid.json');
49
+
50
+ const valid = runJson('node', [validatorScript, 'findings', validPath]);
51
+ assert.equal(valid.ok, true);
52
+
53
+ const invalid = runRaw('node', [validatorScript, 'findings', invalidPath]);
54
+ assert.notEqual(invalid.status, 0);
55
+ assert.match(`${invalid.stdout}${invalid.stderr}`, /\$\[0\]\.claim is required/);
56
+ });
57
+
58
+ test('schema-validate accepts valid skeptic, referee, fix-report, fix-strategy, and fix-plan artifacts', () => {
59
+ const sandbox = makeSandbox('schema-validate-more-');
60
+ const validatorScript = resolveSkillScript('schema-validate.cjs');
61
+ const skepticPath = path.join(sandbox, 'skeptic.json');
62
+ const refereePath = path.join(sandbox, 'referee.json');
63
+ const fixReportPath = path.join(sandbox, 'fix-report.json');
64
+ const fixStrategyPath = path.join(sandbox, 'fix-strategy.json');
65
+ const fixPlanPath = path.join(sandbox, 'fix-plan.json');
66
+
67
+ writeJson(skepticPath, [
68
+ {
69
+ bugId: 'BUG-1',
70
+ response: 'ACCEPT',
71
+ analysisSummary: 'The finding holds after re-reading the code.'
72
+ }
73
+ ]);
74
+ writeJson(refereePath, [
75
+ {
76
+ bugId: 'BUG-1',
77
+ verdict: 'REAL_BUG',
78
+ trueSeverity: 'Critical',
79
+ confidenceScore: 95,
80
+ confidenceLabel: 'high',
81
+ verificationMode: 'INDEPENDENTLY_VERIFIED',
82
+ analysisSummary: 'Confirmed by direct code trace.'
83
+ }
84
+ ]);
85
+ writeJson(fixReportPath, {
86
+ version: '3.0.0',
87
+ fix_branch: 'bug-hunter-fix-20260311-200000',
88
+ base_commit: 'abc123',
89
+ dry_run: false,
90
+ circuit_breaker_tripped: false,
91
+ phase2_timeout_hit: false,
92
+ fixes: [
93
+ {
94
+ bugId: 'BUG-1',
95
+ severity: 'CRITICAL',
96
+ status: 'FIXED',
97
+ files: ['src/a.ts'],
98
+ lines: '10-12',
99
+ commit: 'def456',
100
+ description: 'Parameterized the query.'
101
+ }
102
+ ],
103
+ verification: {
104
+ baseline_pass: 10,
105
+ baseline_fail: 1,
106
+ flaky_tests: 0,
107
+ final_pass: 11,
108
+ final_fail: 0,
109
+ new_failures: 0,
110
+ resolved_failures: 1,
111
+ typecheck_pass: true,
112
+ build_pass: true,
113
+ fixer_bugs_found: 0
114
+ },
115
+ summary: {
116
+ total_confirmed: 1,
117
+ eligible: 1,
118
+ manual_review: 0,
119
+ fixed: 1,
120
+ fix_reverted: 0,
121
+ fix_failed: 0,
122
+ skipped: 0,
123
+ fixer_bug: 0,
124
+ partial: 0
125
+ }
126
+ });
127
+ writeJson(fixStrategyPath, {
128
+ version: '3.1.0',
129
+ generatedAt: '2026-03-12T00:00:00.000Z',
130
+ confidenceThreshold: 75,
131
+ summary: {
132
+ confirmed: 1,
133
+ safeAutofix: 1,
134
+ manualReview: 0,
135
+ largerRefactor: 0,
136
+ architecturalRemediation: 0,
137
+ canaryCandidates: 1,
138
+ rolloutCandidates: 0
139
+ },
140
+ clusters: [
141
+ {
142
+ clusterId: 'cluster-1',
143
+ strategy: 'safe-autofix',
144
+ executionStage: 'canary',
145
+ autofixEligible: true,
146
+ bugIds: ['BUG-1'],
147
+ files: ['src/a.ts'],
148
+ maxSeverity: 'CRITICAL',
149
+ summary: '1 bug(s) in src classified as safe-autofix.',
150
+ recommendedAction: 'Proceed through the guarded fix pipeline with canary verification and rollback safety.',
151
+ reasons: ['Finding is localized enough for a guarded surgical fix.']
152
+ }
153
+ ]
154
+ });
155
+ writeJson(fixPlanPath, {
156
+ generatedAt: '2026-03-12T00:00:00.000Z',
157
+ confidenceThreshold: 75,
158
+ canarySize: 1,
159
+ totals: {
160
+ findings: 1,
161
+ eligible: 1,
162
+ canary: 1,
163
+ rollout: 0,
164
+ manualReview: 0
165
+ },
166
+ canary: [
167
+ {
168
+ bugId: 'BUG-1',
169
+ severity: 'Critical',
170
+ category: 'logic',
171
+ file: 'src/a.ts',
172
+ lines: '10-12',
173
+ claim: 'x',
174
+ evidence: 'src/a.ts:10-12 evidence',
175
+ runtimeTrigger: 'Call x()',
176
+ crossReferences: ['Single file'],
177
+ confidenceScore: 95,
178
+ strategy: 'safe-autofix',
179
+ executionStage: 'canary',
180
+ autofixEligible: true,
181
+ reason: 'Finding is localized enough for a guarded surgical fix.'
182
+ }
183
+ ],
184
+ rollout: [],
185
+ manualReview: []
186
+ });
187
+
188
+ assert.equal(runJson('node', [validatorScript, 'skeptic', skepticPath]).ok, true);
189
+ assert.equal(runJson('node', [validatorScript, 'referee', refereePath]).ok, true);
190
+ assert.equal(runJson('node', [validatorScript, 'fix-report', fixReportPath]).ok, true);
191
+ assert.equal(runJson('node', [validatorScript, 'fix-strategy', fixStrategyPath]).ok, true);
192
+ assert.equal(runJson('node', [validatorScript, 'fix-plan', fixPlanPath]).ok, true);
41
193
  });
@@ -0,0 +1,212 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const test = require('node:test');
5
+
6
+ const {
7
+ makeSandbox,
8
+ resolveSkillScript,
9
+ runJson,
10
+ runRaw
11
+ } = require('./test-utils.cjs');
12
+
13
+ function writeExecutable(filePath, content) {
14
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
15
+ fs.writeFileSync(filePath, content, 'utf8');
16
+ fs.chmodSync(filePath, 0o755);
17
+ }
18
+
19
+ test('pr-scope resolves the current PR via gh metadata', () => {
20
+ const sandbox = makeSandbox('pr-scope-current-');
21
+ const script = resolveSkillScript('pr-scope.cjs');
22
+ const ghPath = path.join(sandbox, 'gh-mock.cjs');
23
+
24
+ writeExecutable(ghPath, `#!/usr/bin/env node
25
+ const args = process.argv.slice(2);
26
+ if (args[0] === 'pr' && args[1] === 'view') {
27
+ process.stdout.write(JSON.stringify({ number: 42, title: 'Fix auth flow', headRefName: 'feature/auth', baseRefName: 'main', url: 'https://example.test/pr/42' }));
28
+ process.exit(0);
29
+ }
30
+ if (args[0] === 'pr' && args[1] === 'diff') {
31
+ process.stdout.write('src/auth.ts\\nsrc/session.ts\\n');
32
+ process.exit(0);
33
+ }
34
+ process.exit(1);
35
+ `);
36
+
37
+ const result = runJson('node', [script, 'resolve', 'current', '--repo-root', sandbox, '--gh-bin', ghPath]);
38
+ assert.equal(result.ok, true);
39
+ assert.equal(result.source, 'gh');
40
+ assert.equal(result.pr.number, 42);
41
+ assert.deepEqual(result.changedFiles, ['src/auth.ts', 'src/session.ts']);
42
+ });
43
+
44
+ test('pr-scope falls back to git when current PR metadata is unavailable', () => {
45
+ const sandbox = makeSandbox('pr-scope-fallback-');
46
+ const script = resolveSkillScript('pr-scope.cjs');
47
+ const ghPath = path.join(sandbox, 'gh-fail.cjs');
48
+ const gitPath = path.join(sandbox, 'git-mock.cjs');
49
+
50
+ writeExecutable(ghPath, `#!/usr/bin/env node
51
+ process.stderr.write('gh unavailable');
52
+ process.exit(1);
53
+ `);
54
+
55
+ writeExecutable(gitPath, `#!/usr/bin/env node
56
+ const args = process.argv.slice(2);
57
+ if (args[0] === 'rev-parse') {
58
+ process.stdout.write('feature/local\\n');
59
+ process.exit(0);
60
+ }
61
+ if (args[0] === 'diff') {
62
+ process.stdout.write('src/local.ts\\n');
63
+ process.exit(0);
64
+ }
65
+ process.exit(1);
66
+ `);
67
+
68
+ const result = runJson('node', [
69
+ script,
70
+ 'resolve',
71
+ 'current',
72
+ '--repo-root',
73
+ sandbox,
74
+ '--gh-bin',
75
+ ghPath,
76
+ '--git-bin',
77
+ gitPath,
78
+ '--base',
79
+ 'develop'
80
+ ]);
81
+ assert.equal(result.ok, true);
82
+ assert.equal(result.source, 'git');
83
+ assert.equal(result.pr.headRefName, 'feature/local');
84
+ assert.equal(result.pr.baseRefName, 'develop');
85
+ assert.deepEqual(result.changedFiles, ['src/local.ts']);
86
+ });
87
+
88
+ test('pr-scope resolves the most recent PR via gh list + diff', () => {
89
+ const sandbox = makeSandbox('pr-scope-recent-');
90
+ const script = resolveSkillScript('pr-scope.cjs');
91
+ const ghPath = path.join(sandbox, 'gh-recent.cjs');
92
+
93
+ writeExecutable(ghPath, `#!/usr/bin/env node
94
+ const args = process.argv.slice(2);
95
+ if (args[0] === 'pr' && args[1] === 'list') {
96
+ process.stdout.write(JSON.stringify([{ number: 7, title: 'Recent PR', headRefName: 'feature/recent', baseRefName: 'main', url: 'https://example.test/pr/7' }]));
97
+ process.exit(0);
98
+ }
99
+ if (args[0] === 'pr' && args[1] === 'diff') {
100
+ process.stdout.write('src/recent.ts\\n');
101
+ process.exit(0);
102
+ }
103
+ process.exit(1);
104
+ `);
105
+
106
+ const result = runJson('node', [script, 'resolve', 'recent', '--repo-root', sandbox, '--gh-bin', ghPath]);
107
+ assert.equal(result.ok, true);
108
+ assert.equal(result.source, 'gh');
109
+ assert.equal(result.pr.number, 7);
110
+ assert.deepEqual(result.changedFiles, ['src/recent.ts']);
111
+ });
112
+
113
+ test('pr-scope uses the discovered default branch for current-branch fallback', () => {
114
+ const sandbox = makeSandbox('pr-scope-default-branch-');
115
+ const script = resolveSkillScript('pr-scope.cjs');
116
+ const ghPath = path.join(sandbox, 'gh-fail.cjs');
117
+ const gitPath = path.join(sandbox, 'git-default.cjs');
118
+
119
+ writeExecutable(ghPath, `#!/usr/bin/env node
120
+ process.stderr.write('gh unavailable');
121
+ process.exit(1);
122
+ `);
123
+
124
+ writeExecutable(gitPath, `#!/usr/bin/env node
125
+ const args = process.argv.slice(2);
126
+ if (args[0] === 'rev-parse' && args[1] === '--abbrev-ref') {
127
+ process.stdout.write('feature/local\\n');
128
+ process.exit(0);
129
+ }
130
+ if (args[0] === 'symbolic-ref') {
131
+ process.stdout.write('refs/remotes/origin/trunk\\n');
132
+ process.exit(0);
133
+ }
134
+ if (args[0] === 'diff' && args[2] === 'origin/trunk...feature/local') {
135
+ process.stdout.write('src/from-trunk.ts\\n');
136
+ process.exit(0);
137
+ }
138
+ process.stderr.write('unexpected command: ' + args.join(' '));
139
+ process.exit(1);
140
+ `);
141
+
142
+ const result = runJson('node', [
143
+ script,
144
+ 'resolve',
145
+ 'current',
146
+ '--repo-root',
147
+ sandbox,
148
+ '--gh-bin',
149
+ ghPath,
150
+ '--git-bin',
151
+ gitPath
152
+ ]);
153
+ assert.equal(result.ok, true);
154
+ assert.equal(result.source, 'git');
155
+ assert.equal(result.pr.baseRefName, 'trunk');
156
+ assert.deepEqual(result.changedFiles, ['src/from-trunk.ts']);
157
+ });
158
+
159
+ test('pr-scope fails current-branch fallback when no trustworthy base branch is available', () => {
160
+ const sandbox = makeSandbox('pr-scope-no-base-');
161
+ const script = resolveSkillScript('pr-scope.cjs');
162
+ const ghPath = path.join(sandbox, 'gh-fail.cjs');
163
+ const gitPath = path.join(sandbox, 'git-partial.cjs');
164
+
165
+ writeExecutable(ghPath, `#!/usr/bin/env node
166
+ process.stderr.write('gh unavailable');
167
+ process.exit(1);
168
+ `);
169
+
170
+ writeExecutable(gitPath, `#!/usr/bin/env node
171
+ const args = process.argv.slice(2);
172
+ if (args[0] === 'rev-parse' && args[1] === '--abbrev-ref') {
173
+ process.stdout.write('feature/local\\n');
174
+ process.exit(0);
175
+ }
176
+ process.stderr.write('missing default branch');
177
+ process.exit(1);
178
+ `);
179
+
180
+ const result = runRaw('node', [
181
+ script,
182
+ 'resolve',
183
+ 'current',
184
+ '--repo-root',
185
+ sandbox,
186
+ '--gh-bin',
187
+ ghPath,
188
+ '--git-bin',
189
+ gitPath
190
+ ], {
191
+ encoding: 'utf8'
192
+ });
193
+ assert.notEqual(result.status, 0);
194
+ assert.match(`${result.stdout || ''}${result.stderr || ''}`, /base branch|default branch|missing default branch/i);
195
+ });
196
+
197
+ test('pr-scope fails for numbered PRs when gh metadata cannot be resolved', () => {
198
+ const sandbox = makeSandbox('pr-scope-numbered-');
199
+ const script = resolveSkillScript('pr-scope.cjs');
200
+ const ghPath = path.join(sandbox, 'gh-fail.cjs');
201
+
202
+ writeExecutable(ghPath, `#!/usr/bin/env node
203
+ process.stderr.write('not found');
204
+ process.exit(1);
205
+ `);
206
+
207
+ const result = runRaw('node', [script, 'resolve', '123', '--repo-root', sandbox, '--gh-bin', ghPath], {
208
+ encoding: 'utf8'
209
+ });
210
+ assert.notEqual(result.status, 0);
211
+ assert.match(`${result.stdout || ''}${result.stderr || ''}`, /not found/);
212
+ });
@@ -0,0 +1,180 @@
1
+ const assert = require('node:assert/strict');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const test = require('node:test');
5
+
6
+ const {
7
+ makeSandbox,
8
+ resolveSkillScript,
9
+ runRaw,
10
+ writeJson
11
+ } = require('./test-utils.cjs');
12
+
13
+ test('render-report renders a markdown summary from findings and referee JSON', () => {
14
+ const sandbox = makeSandbox('render-report-');
15
+ const script = resolveSkillScript('render-report.cjs');
16
+ const findingsPath = path.join(sandbox, 'findings.json');
17
+ const refereePath = path.join(sandbox, 'referee.json');
18
+
19
+ writeJson(findingsPath, [
20
+ {
21
+ bugId: 'BUG-1',
22
+ severity: 'Critical',
23
+ category: 'security',
24
+ file: 'src/api.ts',
25
+ lines: '10-12',
26
+ claim: 'User input reaches an unsafe sink',
27
+ evidence: 'src/api.ts:10-12 ...',
28
+ runtimeTrigger: 'POST /api with attacker input',
29
+ crossReferences: ['Single file'],
30
+ confidenceScore: 90
31
+ }
32
+ ]);
33
+
34
+ writeJson(refereePath, [
35
+ {
36
+ bugId: 'BUG-1',
37
+ verdict: 'REAL_BUG',
38
+ trueSeverity: 'Critical',
39
+ confidenceScore: 91,
40
+ confidenceLabel: 'high',
41
+ verificationMode: 'INDEPENDENTLY_VERIFIED',
42
+ analysisSummary: 'Confirmed by tracing the sink.'
43
+ }
44
+ ]);
45
+
46
+ const result = runRaw('node', [script, 'report', findingsPath, refereePath], {
47
+ encoding: 'utf8'
48
+ });
49
+
50
+ assert.equal(result.status, 0);
51
+ assert.match(result.stdout, /# Bug Hunter Report/);
52
+ assert.match(result.stdout, /BUG-1 \| Critical \| src\/api.ts/);
53
+ assert.match(result.stdout, /Confirmed by tracing the sink/);
54
+ });
55
+
56
+ test('render-report renders coverage markdown from coverage JSON', () => {
57
+ const sandbox = makeSandbox('render-coverage-');
58
+ const script = resolveSkillScript('render-report.cjs');
59
+ const coveragePath = path.join(sandbox, 'coverage.json');
60
+
61
+ writeJson(coveragePath, {
62
+ schemaVersion: 1,
63
+ iteration: 2,
64
+ status: 'COMPLETE',
65
+ files: [{ path: 'src/a.ts', status: 'done' }],
66
+ bugs: [{ bugId: 'BUG-1', severity: 'Low', file: 'src/a.ts', claim: 'example' }],
67
+ fixes: [{ bugId: 'BUG-1', status: 'MANUAL_REVIEW' }]
68
+ });
69
+
70
+ const result = runRaw('node', [script, 'coverage', coveragePath], {
71
+ encoding: 'utf8'
72
+ });
73
+
74
+ assert.equal(result.status, 0);
75
+ assert.match(result.stdout, /# Bug Hunter Coverage/);
76
+ assert.match(result.stdout, /done \| src\/a.ts/);
77
+ assert.match(result.stdout, /BUG-1 \| Low \| src\/a.ts \| example/);
78
+ });
79
+
80
+ test('render-report renders a markdown summary from fix-report JSON', () => {
81
+ const sandbox = makeSandbox('render-fix-report-');
82
+ const script = resolveSkillScript('render-report.cjs');
83
+ const fixReportPath = path.join(sandbox, 'fix-report.json');
84
+
85
+ writeJson(fixReportPath, {
86
+ version: '3.0.0',
87
+ fix_branch: 'bug-hunter-fix-20260311-200000',
88
+ base_commit: 'abc123',
89
+ dry_run: false,
90
+ circuit_breaker_tripped: false,
91
+ phase2_timeout_hit: false,
92
+ fixes: [
93
+ {
94
+ bugId: 'BUG-1',
95
+ severity: 'CRITICAL',
96
+ status: 'FIXED',
97
+ files: ['src/a.ts'],
98
+ lines: '10-12',
99
+ commit: 'def456',
100
+ description: 'Parameterized the query.'
101
+ }
102
+ ],
103
+ verification: {
104
+ baseline_pass: 10,
105
+ baseline_fail: 1,
106
+ flaky_tests: 0,
107
+ final_pass: 11,
108
+ final_fail: 0,
109
+ new_failures: 0,
110
+ resolved_failures: 1,
111
+ typecheck_pass: true,
112
+ build_pass: true,
113
+ fixer_bugs_found: 0
114
+ },
115
+ summary: {
116
+ total_confirmed: 1,
117
+ eligible: 1,
118
+ manual_review: 0,
119
+ fixed: 1,
120
+ fix_reverted: 0,
121
+ fix_failed: 0,
122
+ skipped: 0,
123
+ fixer_bug: 0,
124
+ partial: 0
125
+ }
126
+ });
127
+
128
+ const result = runRaw('node', [script, 'fix-report', fixReportPath], {
129
+ encoding: 'utf8'
130
+ });
131
+
132
+ assert.equal(result.status, 0);
133
+ assert.match(result.stdout, /# Fix Report/);
134
+ assert.match(result.stdout, /BUG-1 \| FIXED \| CRITICAL/);
135
+ assert.match(result.stdout, /Parameterized the query/);
136
+ });
137
+
138
+ test('render-report renders a markdown summary from fix-strategy JSON', () => {
139
+ const sandbox = makeSandbox('render-fix-strategy-');
140
+ const script = resolveSkillScript('render-report.cjs');
141
+ const fixStrategyPath = path.join(sandbox, 'fix-strategy.json');
142
+
143
+ writeJson(fixStrategyPath, {
144
+ version: '3.1.0',
145
+ generatedAt: '2026-03-12T00:00:00.000Z',
146
+ confidenceThreshold: 75,
147
+ summary: {
148
+ confirmed: 2,
149
+ safeAutofix: 1,
150
+ manualReview: 1,
151
+ largerRefactor: 0,
152
+ architecturalRemediation: 0,
153
+ canaryCandidates: 1,
154
+ rolloutCandidates: 0
155
+ },
156
+ clusters: [
157
+ {
158
+ clusterId: 'cluster-1',
159
+ strategy: 'safe-autofix',
160
+ executionStage: 'canary',
161
+ autofixEligible: true,
162
+ bugIds: ['BUG-1'],
163
+ files: ['src/a.ts'],
164
+ maxSeverity: 'CRITICAL',
165
+ summary: '1 bug(s) in src classified as safe-autofix.',
166
+ recommendedAction: 'Proceed through the guarded fix pipeline with canary verification and rollback safety.',
167
+ reasons: ['Finding is localized enough for a guarded surgical fix.']
168
+ }
169
+ ]
170
+ });
171
+
172
+ const result = runRaw('node', [script, 'fix-strategy', fixStrategyPath], {
173
+ encoding: 'utf8'
174
+ });
175
+
176
+ assert.equal(result.status, 0);
177
+ assert.match(result.stdout, /# Fix Strategy/);
178
+ assert.match(result.stdout, /cluster-1 \| safe-autofix \| canary/);
179
+ assert.match(result.stdout, /guarded fix pipeline/);
180
+ });