Skip to content

feat(report): redesign hunt report to match the run report - #233

Merged
arunSunnyKVS merged 2 commits into
KeyValueSoftwareSystems:masterfrom
jithin23-kv:feat/hunt-report-redesign
Aug 4, 2026
Merged

feat(report): redesign hunt report to match the run report#233
arunSunnyKVS merged 2 commits into
KeyValueSoftwareSystems:masterfrom
jithin23-kv:feat/hunt-report-redesign

Conversation

@jithin23-kv

@jithin23-kv jithin23-kv commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

The opfor hunt report and the opfor run report read as two different products.

core/src/autonomous/report/html.ts had drifted from core/src/report/render.ts on almost every axis: a different accent colour (amber #f5ad5c vs the brand red #FF4D4F), a navy gradient cover instead of the black band, a generic shield glyph instead of the Opfor wordmark, heavy box-shadow on every card, a smaller type scale, and a bespoke turn view with no navigation. Anyone running both commands got two visibly unrelated documents.

Three concrete defects behind that:

  1. Brand assets were duplicated. Both renderers inlined their own copy of the wordmark + icon SVGs, so a rebrand had to be done twice and could silently drift.
  2. The scout and verifier models could not be rendered. opfor hunt runs three agents (commander / operator / scout, --scout-model defaulting to haiku) plus a separate verifier for self-checks, but AutonomousReport only declared commanderModel and operatorModel. The other two were configurable yet absent from the report type, so the report claimed two agents where there were four.
  3. Defended threads had no viewable transcript. Confirmed findings got full cards; PASS/ERROR threads got a bare threadId chip. Since most hunts finish with zero confirmed findings, the common case produced a report with no inspectable conversation at all.

Separately, the executive summary in both reports rendered its labels at different heights: .exec-strip-item used justify-content:center, so each card vertically centred its own content, and cards hold differing amounts (verdict + risk badge + subline vs. gauge vs. value + dot rows). The labels stepped downward across the strip.

Solution

Rebuild the hunt renderer on the run report's visual language — black cover band, shared wordmark, red accent, numbered section rhythm, executive strip with the semicircular safety gauge, and the collapsible transcript with a turn rail (click-to-jump + scroll-spy) — on a slightly roomier type scale, because a hunt carries far more per screen than a suite run (9 sections vs. 3).

Hunt-specific content is kept, not dropped, and rebuilt on the shared card/table primitives instead of bespoke CSS: recon fingerprint with guardrails/weak-points, vulnerability-class matrix, attack tree, recommendations, response patterns, invented techniques, and the decision log.

Two additions on top of the design port:

  • Breach turns are marked red on the turn rail, so a long thread can be navigated straight to where it broke, with a gradual fade at whichever rail edge still has turns hidden.
  • Every thread now gets a card with its transcript — confirmed first (worst severity, then confidence), then defended, then errored.

Safety score is derived as the inverse of attack success rate (N/A when nothing was conclusively scored) so hunt reports carry the same 0–100 headline metric as run reports. Duration is computed from startedAtgeneratedAt, guarded because older reports predate startedAt.

Changes

core/ (@keyvaluesystems/agent-opfor-core)

File Change
core/src/report/brand.ts New. Shared inline brand assets (Opfor wordmark + lock/copy/attacker/agent/severity/chevron icons). Self-contained SVG — reports still fetch no external assets.
core/src/report/render.ts Imports the shared assets instead of inlining them (−39 lines, verified byte-identical before the move, so the run report is visually unchanged). Executive-summary label alignment fix.
core/src/autonomous/report/html.ts Rewritten on the run report's visual language.
core/src/autonomous/report/types.ts AutonomousReport gains optional scoutModel and verifierModel. Optional so existing report JSON still parses.
core/src/autonomous/orchestrator/run.ts Populates both on the returned report; verifier falls back to the commander model, matching selfCheck.ts.

No runner changes — writeAutonomousReport and the CLI/SDK entry points are untouched.

Issue

N/A

How to test

npm run build
npm run typecheck
npm test

Then render a hunt report. Any existing run works — no need to spend a live hunt:

node -e "
const { readFileSync, writeFileSync } = require('node:fs');
import('./core/dist/autonomous/report/html.js').then(({ renderReportHtml }) => {
  const p = process.argv[1];
  writeFileSync('/tmp/hunt.html', renderReportHtml(JSON.parse(readFileSync(p, 'utf8'))));
  console.log('wrote /tmp/hunt.html');
});
" .opfor/reports/hunt-report-*/[!d]*-report.json

Or end to end: opfor hunt --endpoint <url> --objective "<text>".

Worth checking:

  • Cover + accent match opfor run output side by side (black band, Opfor wordmark, red accent).
  • Executive summary labels sit on one baseline in both report types — this was the stepped-label bug.
  • Turn rail: breach turns are red; clicking one jumps to that turn; scrolling highlights the turn in view; the fade appears at whichever edge still hides turns. Verify on a long thread (15+ turns).
  • Assessment Scope → Attack Agents lists all four roles. Reports generated before this PR correctly show for scout/verifier; a fresh opfor hunt populates them.
  • A zero-findings hunt still shows defended threads as cards with transcripts.
  • Run report is unchanged apart from the label alignment — the brand extraction should be invisible.

Verified locally against two real hunt reports (one with 6 confirmed findings, one fully defended) and two real run reports: no HTML tag mismatches, no undefined/NaN leaks, correct fallbacks on reports missing startedAt/scoutModel, and 188 tests passing.

Screenshots

Summary by CodeRabbit

  • New Features
    • Introduced a redesigned autonomous hunt report with responsive layouts, navigation, scope and reconnaissance details, findings cards, attack trees, recommendations, appendices, and footer metadata.
    • Added expandable transcripts with breach highlighting, turn navigation, and corroboration details.
    • Reports now display scout and verifier models, including fallback information when applicable.
    • Added safety scoring, verdict and error handling, vulnerability matrices, standards labels, and duration details.
  • Style
    • Improved report branding with consistent logos, icons, gauges, and executive summary layouts.

The autonomous hunt report had drifted into a visually separate product from
the `opfor run` report: its own colour tokens (amber accent vs red), its own
type scale, a generic shield glyph instead of the Opfor wordmark, heavy card
shadows, and a bespoke turn view with no navigation. Both renderers also
inlined their own copy of the brand SVGs, so any rebrand had to happen twice.

Rebuild the hunt renderer on the run report's visual language — black cover
band, shared wordmark, red accent, section rhythm, exec strip with the safety
gauge, and the transcript + turn rail with scroll-spy — on a slightly roomier
type scale, since a hunt carries far more per screen than a suite run.

Hunt-specific sections (recon fingerprint, vuln-class matrix, attack tree,
decision log, inventions) are kept, rebuilt on the shared card primitives.

Also:
- Extract the wordmark + icons to report/brand.ts so both renderers share
  one copy. Verified byte-identical before moving; the run report is
  visually unchanged by it.
- Surface the scout and verifier models. Both were configurable but absent
  from AutonomousReport, so neither could be rendered; the report showed
  only commander and operator despite hunt running three agents.
- Give every thread a finding card with its transcript. Defended threads
  were previously bare chips, so runs with no confirmed findings — the
  common case — had no inspectable transcripts at all.
- Fix executive-summary label alignment in both reports: exec-strip-item
  centred its content vertically, so cards with differing content heights
  put every label at a different height.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The PR adds scout and verifier model metadata to autonomous reports. It replaces the autonomous HTML report with a responsive hunt report containing scoring, navigation, findings, recommendations, appendices, and interactive transcripts. Shared formatting utilities and SVG branding assets support report rendering.

Changes

Autonomous report

Layer / File(s) Summary
Report metadata contract and population
core/src/autonomous/report/types.ts, core/src/autonomous/orchestrator/run.ts
AutonomousReport now includes scout and verifier model fields. Report generation records both values and falls back to the commander model for the verifier when verification is enabled.
Shared report formatting and branding
core/src/report/format.ts, core/src/report/brand.ts, core/src/report/render.ts
Shared utilities now handle escaping, formatting, severity ordering, safety gauges, transcript labels, and inline SVG assets. The standard report adjusts executive summary and gauge alignment.
Hunt report structure and interactions
core/src/autonomous/report/html.ts
The HTML report now includes verdict handling, safety scoring, duration, dynamic navigation, vulnerability categories, finding transcripts, attack trees, recommendations, appendices, and responsive transcript controls.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant runAutonomous
  participant AutonomousReport
  participant autonomousHtmlReport
  participant reportFormat
  runAutonomous->>AutonomousReport: record scoutModel and verifierModel
  AutonomousReport->>autonomousHtmlReport: provide report data
  autonomousHtmlReport->>reportFormat: format and escape report values
  reportFormat-->>autonomousHtmlReport: return formatted values and SVG gauge
  autonomousHtmlReport-->>AutonomousReport: render hunt report HTML
Loading

Possibly related PRs

Suggested reviewers: arunsunnykvs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: redesigning the hunt report to match the run report.
Description check ✅ Passed The description covers the problem, solution, changes, issue status, testing steps, and expected behavior, with only screenshots left as a placeholder.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
core/src/autonomous/orchestrator/run.ts (1)

340-341: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider recording verifierModel only when verification ran.

Line 97 sets verifyEnabled = options.verify && Boolean(process.env.ANTHROPIC_API_KEY). When verifyEnabled is false, no self_check tool is granted and no verifier runs. Line 341 still records a verifier model. The hunt report then renders a "Verifier — independent self-check" row (see core/src/autonomous/report/html.ts line 771) for a run that performed no verification.

Gate the field on verifyEnabled so the report reflects what actually ran.

♻️ Proposed change
   report.scoutModel = options.scoutModel;
-  report.verifierModel = options.verifierModel ?? options.commanderModel;
+  report.verifierModel = verifyEnabled
+    ? (options.verifierModel ?? options.commanderModel)
+    : undefined;

The renderer already falls back to "—" when the field is unset.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/autonomous/orchestrator/run.ts` around lines 340 - 341, Update the
report model assignment in the run orchestration flow to set
report.verifierModel only when verifyEnabled is true; otherwise leave it unset
so the renderer displays its existing fallback. Keep report.scoutModel
assignment and the existing verifier-model fallback unchanged.
core/src/autonomous/report/html.ts (1)

28-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Finish the shared-report extraction that core/src/report/brand.ts started. This PR moved the static SVG assets into core/src/report/brand.ts but left the behavioral report code copied between core/src/autonomous/report/html.ts and core/src/report/render.ts. Both copies will drift as one report evolves. One extraction removes both duplicates.

  • core/src/autonomous/report/html.ts#L28-L49: move truncate, formatDuration, safetyColor, gaugeSvg (lines 60-69), and roleLabel (lines 83-85) into a shared module such as core/src/report/format.ts, then import them here and in core/src/report/render.ts.
  • core/src/autonomous/report/html.ts#L866-L949: extract the copy-button handler and the transcript toggle plus turn-rail scroll-spy into a shared script constant, and keep only the rail-fade additions local to this report.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/autonomous/report/html.ts` around lines 28 - 49, The report helpers
truncate, formatDuration, safetyColor, gaugeSvg, and roleLabel are duplicated
across report implementations; move them into a shared core/src/report/format.ts
module, then import and use them from core/src/autonomous/report/html.ts (28-49)
and core/src/report/render.ts. Also extract the shared copy-button,
transcript-toggle, and turn-rail scroll-spy behavior from
core/src/autonomous/report/html.ts (866-949) into a shared script constant,
leaving only rail-fade additions local to html.ts.
core/src/report/brand.ts (1)

7-38: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider namespacing the gradient ids.

The logo defines twelve gradient ids named paint0_r through paint11_r, and the paths reference them with fill="url(#paintN_r)". SVG ids share the HTML document id space. Both renderers embed the logo once per document today (core/src/report/render.ts line 435 and core/src/autonomous/report/html.ts line 690), so the references resolve correctly.

The names come from a default export and are likely to collide if a report later embeds the logo twice or embeds a second exported SVG. Prefix the ids, for example opfor-logo-grad-0, to make the asset safe to reuse.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/report/brand.ts` around lines 7 - 38, Namespace all gradient IDs in
OPFOR_LOGO_SVG with an asset-specific prefix, such as opfor-logo-grad-0 through
opfor-logo-grad-11, and update every corresponding fill="url(#...)" reference to
match. Keep the gradient definitions and visual output unchanged while ensuring
repeated logo embeddings cannot collide.
core/src/report/render.ts (1)

320-320: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused .gauge-sub rule.

.gauge-sub is only defined in core/src/report/render.ts and is not emitted by the report template.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/report/render.ts` at line 320, Remove the unused .gauge-sub CSS rule
from the report styles in render.ts, leaving the report template and all other
style rules unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/src/autonomous/report/html.ts`:
- Around line 830-836: Update the Findings subtitle in the findings section to
use labels that explicitly identify its values as finding counts, distinguishing
them from the thread counts shown in the Executive Summary. Preserve the
existing fails, defended, and errored calculations and conditional rail-marker
text while removing ambiguity around “confirmed” and “defended.”

---

Nitpick comments:
In `@core/src/autonomous/orchestrator/run.ts`:
- Around line 340-341: Update the report model assignment in the run
orchestration flow to set report.verifierModel only when verifyEnabled is true;
otherwise leave it unset so the renderer displays its existing fallback. Keep
report.scoutModel assignment and the existing verifier-model fallback unchanged.

In `@core/src/autonomous/report/html.ts`:
- Around line 28-49: The report helpers truncate, formatDuration, safetyColor,
gaugeSvg, and roleLabel are duplicated across report implementations; move them
into a shared core/src/report/format.ts module, then import and use them from
core/src/autonomous/report/html.ts (28-49) and core/src/report/render.ts. Also
extract the shared copy-button, transcript-toggle, and turn-rail scroll-spy
behavior from core/src/autonomous/report/html.ts (866-949) into a shared script
constant, leaving only rail-fade additions local to html.ts.

In `@core/src/report/brand.ts`:
- Around line 7-38: Namespace all gradient IDs in OPFOR_LOGO_SVG with an
asset-specific prefix, such as opfor-logo-grad-0 through opfor-logo-grad-11, and
update every corresponding fill="url(#...)" reference to match. Keep the
gradient definitions and visual output unchanged while ensuring repeated logo
embeddings cannot collide.

In `@core/src/report/render.ts`:
- Line 320: Remove the unused .gauge-sub CSS rule from the report styles in
render.ts, leaving the report template and all other style rules unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 75b98b40-dfa6-41fd-abc0-3fae754b107b

📥 Commits

Reviewing files that changed from the base of the PR and between 04d0690 and aabe5b6.

📒 Files selected for processing (5)
  • core/src/autonomous/orchestrator/run.ts
  • core/src/autonomous/report/html.ts
  • core/src/autonomous/report/types.ts
  • core/src/report/brand.ts
  • core/src/report/render.ts

Comment thread core/src/autonomous/report/html.ts
Attack Tree claimed "9 threads" directly above a tree rendering 6 nodes.
summary.threads is set to findings.length in mapRunLog, but one thread can
produce several findings, so every thread-labelled number was really a finding
count. Derive the true count from distinct threadIds and use it for the tree
subtitle, the exploration row, and the narrative; headline the executive card
as "Findings" since its dots are per-finding verdicts, with the thread count as
its subline. Also say "confirmed findings" in the Findings subtitle so it can't
be read as a thread count.

Record verifierModel only when verification actually ran. verifyEnabled gates
whether the self_check tool is granted, so recording it unconditionally made
the report advertise a verifier for runs that never verified.

Finish the extraction brand.ts started: esc, truncate, formatDuration,
formatTokenCount, safetyColor, gaugeSvg, roleLabel and SEV_HEX/SEV_ORDER were
byte-identical copies in both renderers. They now live in report/format.ts.
Confirmed behaviour-neutral by diffing rendered output before and after.

Namespace the logo's twelve gradient ids (paintN_r → opfor-logo-grad-N). SVG
ids share the document id space, so the generic names would collide if a report
ever embedded the wordmark twice.

Drop the unused .gauge-sub rule, which no template emitted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jithin23-kv

Copy link
Copy Markdown
Collaborator Author

Went through the four nitpicks — three applied as suggested, one applied in part. All in 44be1b4.

run.ts — record verifierModel only when verification ran

Valid, and a good catch. verifyEnabled gates whether self_check is granted to either agent (lines 130 and 162), so recording the model unconditionally made the report advertise a verifier for runs that never verified. Now gated exactly as suggested; the renderer's fallback covers the unset case.

brand.ts — namespace the gradient ids

Applied: paintN_ropfor-logo-grad-N across all twelve definitions and their twelve url(#…) references. Verified every reference still resolves to a defined id and no paintN_r remains. Agreed on the reasoning — the names came straight out of the design-tool export and would collide the moment a document embedded the wordmark twice.

render.ts — remove the unused .gauge-sub rule

Confirmed dead (defined in the stylesheet, emitted by no template, no reference anywhere in core/ or runners/) and removed.

html.ts — finish the extraction brand.ts started

Right that duplication would drift, so I did the pure-function half. Confirmed byte-identical (whitespace-normalised) between the two renderers before moving: esc, truncate, formatDuration, formatTokenCount, safetyColor, gaugeSvg, roleLabel, plus the duplicated SEV_HEX/SEV_ORDER constants. They now live in core/src/report/format.ts and both renderers import from it. Net −152 lines of duplication.

To be sure the move changed nothing, I rendered a real run report before and after and diffed: after normalising the gradient rename and the deleted .gauge-sub line, the output is byte-identical.

I've left the inline <script> blocks separate for now, deliberately. They've genuinely diverged rather than being copies:

  • the hunt script carries refreshRailFade and revealStep for the rail-fade affordance, which the run report has no markup for;
  • the transcript-toggle handlers scope differently — closest(".eval-body") in hunt vs closest(".detail-section, .eval-body") in the run report.

Sharing them today means a parameterised constant to bridge two behaviours, on the run report — the one people already depend on — for modest gain. Happy to do it as a follow-up once both reports have settled, and it's the natural next step after brand.ts + format.ts if you'd rather see it in this PR.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
core/src/report/format.ts (1)

49-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make SEV_HEX exhaustive over Severity.

Record<string, string> does not require a color for every Severity. A future severity can therefore produce undefined in generated CSS. If Severity is the intended palette domain, use Record<Severity, string>.

Proposed type tightening
-export const SEV_HEX: Record<string, string> = {
+export const SEV_HEX: Record<Severity, string> = {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/report/format.ts` around lines 49 - 54, Update the SEV_HEX
declaration to use Record<Severity, string> instead of Record<string, string>,
ensuring every Severity value requires an explicit color entry while preserving
the existing palette mappings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@core/src/report/format.ts`:
- Around line 49-54: Update the SEV_HEX declaration to use Record<Severity,
string> instead of Record<string, string>, ensuring every Severity value
requires an explicit color entry while preserving the existing palette mappings.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4445f995-7bfc-44a0-a3c5-95c2f278c5b6

📥 Commits

Reviewing files that changed from the base of the PR and between aabe5b6 and 44be1b4.

📒 Files selected for processing (5)
  • core/src/autonomous/orchestrator/run.ts
  • core/src/autonomous/report/html.ts
  • core/src/report/brand.ts
  • core/src/report/format.ts
  • core/src/report/render.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • core/src/report/brand.ts
  • core/src/autonomous/orchestrator/run.ts
  • core/src/report/render.ts
  • core/src/autonomous/report/html.ts

@arunSunnyKVS
arunSunnyKVS merged commit c1ecca8 into KeyValueSoftwareSystems:master Aug 4, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants