Skip to content

Let the agent make its case over Radar evidence - #1715

Merged
nadaverell merged 25 commits into
mainfrom
feat/findings-agent-case
Sep 10, 2026
Merged

nadaverell merged 25 commits into
mainfrom
feat/findings-agent-case

Conversation

@nadaverell

@nadaverell nadaverell commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Supersedes #1691, whose content merged into the feat/findings-next-layer integration branch and so never reached main.

Summary

The investigation agent can now make its case on top of Radar's evidence instead of beside it. Its final JSON carries up to eight (ref, role, claim, subject) items and up to five ruled-out hypotheses; Findings renders a role chip and a one-sentence agent note on the card each item names, orders the main list by role, and lists ruled-out hypotheses under the assessment they argue for.

The boundary is the point. The agent can order and frame. It can never author a card, hide one, collapse one, or change its tone, and it can never turn Radar's own contradiction warning into an all-clear. Every card still comes from a tool result Radar recorded.

What changed

Go (internal/ai)

  • The prompt adds evidence and ruled_out alongside the unchanged root_cause_evidence_refs, with instructions on the six roles (cause, symptom, context, benign, demoted, rules_out), one-sentence claims, and subjects for results that fan out into several observations.
  • parse.go reads both arrays with their caps (8 items, 5 hypotheses, 200-char claims). A malformed item stays at its index as invalid rather than being dropped, so evidence_index keeps pointing where the agent meant.
  • runs.go shares one turn-ledger match table between the legacy root-cause binder and the new case binder. A failing item becomes status: "unlinked" on its own, without taking the others down; binding runs for every assessment, healthy and inconclusive included. Subject and claim are copied verbatim as untrusted text.
  • The explanation prompt passes saved roles and claims as read-only notes.

A word for "this is not a live problem"

benign is the only role that says an adverse-looking result does not indicate an active problem. "Excludes a hypothesis" (rules_out) and "is peripheral here" (demoted) are different statements, both of which stay true of a problem that is still burning, so neither reconciles a healthy verdict with evidence contradicting it.

Because the distinction only pays off if the agent knows to reach for it, the prompt states the consequence: when it sets healthy=true, Radar compares the verdict against what it captured and warns the operator about any active-problem evidence left unaddressed, so it must cite each such result and mark it benign with the reason — a failing probe, a restart or crash, an unready workload, an error or panic in a log stream, a Warning event, a failed DNS or network check. Citing only the results that look fine leaves the warning standing, which is the correct outcome when the adverse ones have not been explained.

Placing a claim (investigationCase.ts)

Each item resolves to exactly one observation by (ref, subject). Every discriminator both sides state must match — a core resource has a known empty group, a cluster-scoped one a known empty namespace, and producer-unstated fields are wildcards. Exactly one match pins the claim: on the card head for the group's latest observation, on its own revision row for a superseded read. Anything else renders beside the source in Assessment details rather than guessing.

Omitting a subject and supplying a broken one are different requests. Omission asks Radar to place the note wherever the cited call produced; a subject that does not validate asked for something specific that does not resolve, so the item stays at its source instead of landing on whichever observation happens to be the only one.

The pane (InvestigationEvidencePane.tsx)

  • Selection is legacy links ∊ placed items. Within main, roles order cause → symptom → unlabelled and ruled-out in Radar's order → context → benign → demoted.
  • A cause card sits on the elevated surface with a shadow, so the card the agent argues from carries visual weight without borrowing a severity colour it was never assigned.
  • The "Ruled out" block sits under the Assessment, with what the agent considered and rejected next to the conclusion it argues for, rather than after the evidence list among the withheld counts.
  • A broader group is promoted for an item only when the item's subject, or the cited call itself, named one resource — a subject-less item on a broad query only proves the query returned a row.
  • AssessmentSources rows show roles and any source-placed claim; earlier assessments list their placed claims read-only with the observation each was bound to. A claim whose card the pane withheld is shown here rather than counted as visible on a card, so it is never absent from both.

Radar's own warning stays Radar's

A healthy verdict that contradicts captured adverse evidence raises Radar's warning, computed from the evidence alone. An agent explanation reframes that warning; it never replaces it with reassurance. The banner stays amber, still states that Radar captured evidence of an active problem, points at the notes, and says the evidence is unchanged in the list below. It never names a severity tier, because none was checked.

To reframe it at all, a note must clear five deterministic conditions. None judges whether the agent is right — which is exactly why the warning remains a warning:

  1. it belongs to the assessment on screen, not a later answer or an assessment already replaced;
  2. it carries text, so the banner cannot cite a note that renders nothing;
  3. it sits on the card's current state, not a superseded read of it;
  4. its role is benign, the one role that asserts the evidence is not a live problem;
  5. the same assessment does not also call that card a cause or symptom — self-contradiction is not an explanation.

Every conflicting card must qualify, and the qualifying note must still be one the pane renders. observe() deliberately partitions a log stream read through two different calls into separate groups, so a same-named pod from another workload cannot inherit the target's relevance; that partition must not also decide whether the agent addressed the stream, so groups sharing a kind and identity count as the same observation for this question.

Whose claim is whose, across turns

  • A later turn's citations widen the pane's selection and nothing else, carrying the source that cited them so a newly cited broader card is promoted rather than selected-but-withheld.
  • The displayed assessment keeps its own qualification banner and its own "Used for assessment" markers. The live turn keeps its own resolution and its own case for its own receipt, so an answer lists what that answer cited and not what the pane carried onto it.
  • When a superseded assessment's notes are carried onto cards a later turn did not address, the newest assessment that spoke about a group carries everything it said about that group, not only its first note.

Presentation

Everything the agent contributed to a card lives in one attributed row beneath Radar's fact: a single badge carrying a sparkle and the role, then the sentence, sharing the sentence's baseline. The role travelled with the card title once, where it sat in the same slot as Radar's own badges and could be read as Radar having established it. A rules_out note carries the hypothesis it excludes as text beside the chip — "Rules out" alone forces the reader to find the hypothesis further down and match it back — and only for that role, since a rejected hypothesis stated as bare fact on any other card reads as a claim the agent believes.

Badge gains a muted-accent agent tone (documented in DESIGN.md) — never a severity hue, so agent framing cannot read as a Radar finding. docs/mcp.md describes the case fields, notes that evidence / ruledOut are omitempty and must be treated as optional, and records that a hosted backend sending no evidence renders exactly as before.

Testing

  • make tsc ✓ · go test ./... both modules ✓ · web 1078 tests across 88 files ✓ · packages/k8s-ui 3603 tests across 178 files ✓ · git diff --check clean.
  • New: investigationCase.test.tsx and internal/ai/case_test.go, plus additions to investigationState.test.ts, parts.test.tsx and InvestigationEvidencePane.test.tsx. They cover role parsing and caps, malformed items holding their index, per-item unlinking, a malformed subject staying at its source, subject discrimination on group and namespace, placement on head vs revision row vs assessment details, role ordering, every condition on the conflict banner, carrying all of a winning assessment's notes, a follow-up widening selection without removing any, and a withheld claim falling back to its source row.
  • Visual test against live GCP and AWS clusters across parallel investigations, at light, dark and 900px — covering all six roles, a revision-placed claim, a source-placed claim, a ruled-out block, and the case rendered alongside the diagnose vitals charts.
  • The reframe path was exercised end to end on a live run where the agent returned a healthy verdict over captured adverse evidence and marked each conflicting result benign: the banner stays amber and still points at the evidence, which remains in the list unchanged.

Scope and tradeoffs

  • A diagnose bundle carries up to three vitals charts for one resource, so observation accepts a category qualifier (metrics:cpu, metrics:memory, metrics:restarts) to name one. The bare metrics still works when the bundle captured a single chart. The prompt tells the agent so.
  • A subject-less item on a broad query whose single row is about another resource is placed but stays withheld with that row, and its claim is not shown. The prompt asks for a subject in exactly this case.
  • A firing Prometheus alert is not among the evidence kinds the conflict predicate considers, so it cannot raise the warning. Pre-existing, and worth revisiting now that alerts are projected as evidence.
  • The reframed banner states the situation but offers no action beyond reading on; it is longer than a banner wants to be.
  • The investigation panel's minimum width at narrow viewports predates this change.

Open product questions

Unresolved taste calls, not defects:

  • rules_out is a role name in the wire contract and "Rules out" on screen. It reads as jargon next to "Not a problem" and "Less relevant"; a better word has not surfaced.
  • The demoted role renders as "Less relevant" — keep that label, or name the role?

Note

Medium Risk
Changes diagnosis parsing, server-side evidence binding, and complex multi-turn Findings logic where agent framing must not be mistaken for Radar authority; extensive tests but high interaction surface for investigations.

Overview
Investigation diagnoses can now include an agent case: up to eight evidence items (ref, role, one-sentence claim, optional subject) and up to five ruled_out hypotheses tied to evidence indices. The Go layer parses and caps this JSON, then binds each ref through the same turn-ledger rules as root_cause_evidence_refs; failed items become unlinked without dropping siblings. The prompt and explanation flow are extended so roles/claims are read-only in follow-up explanations.

Findings resolves each item to a single observation when possible and renders agent role chips and claim notes on cards (or on revision rows / Assessment details when placement is ambiguous). Main evidence ordering follows roles; a Ruled out block links to the contradicting observation. Selection still cannot hide or recolor Radar cards; a benign note on every conflicting card can reframe (not remove) the healthy-vs-adverse warning. Follow-up turns widen cited evidence; earlier assessments’ card notes merge unless a later turn addressed the same group.

Adds Badge tone agent, API/types, investigationCase resolver, and docs in DESIGN.md / docs/mcp.md. Hosted backends with no evidence behave as before.

Reviewed by Cursor Bugbot for commit d8531a6. Bugbot is set up for automated code reviews on this repo. Configure here.

@nadaverell
nadaverell requested a review from hisco as a code owner September 9, 2026 22:33
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Frame Radar evidence with agent roles and ruled-out hypotheses

âœĻ Enhancement 🧊 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

â€Ē Bind agent roles, claims, subjects, and ruled-out hypotheses to turn-scoped Radar results.
â€Ē Place uniquely resolved claims on Findings cards without changing Radar-owned content or severity.
â€Ē Order evidence by role and expose unresolved claims through assessment sources.
Diagram

graph TD
A["Agent JSON"] --> B["Case parser"] --> C["Ledger binder"] --> D["Diagnosis API"] --> E["Case resolver"] --> F["Findings pane"]
D --> G["Explanation prompt"]
E --> H["Assessment sources"]
Loading
High-Level Assessment

The PR's layered approach is appropriate: the server authenticates references while the frontend performs observation placement using its existing projection model. Rendering separate agent-authored cards would weaken provenance, while moving placement entirely server-side would duplicate frontend grouping and revision semantics. Keeping agent framing additive and unable to hide, collapse, or recolor Radar evidence is the safest design.

Files changed (16) +2544 / -114

Enhancement (11) +1185 / -114
diagnoser.goDefine agent evidence case types and prompt schema +83/-5

Define agent evidence case types and prompt schema

â€Ē Introduces evidence roles, observation subjects, linked/unlinked case items, ruled-out hypotheses, and private untrusted request structures. Extends the diagnosis JSON instruction with bounded case-generation guidance.

internal/ai/diagnoser.go

explanation.goPass saved case notes into explanation prompts +29/-4

Pass saved case notes into explanation prompts

â€Ē Includes linked evidence claims and ruled-out hypotheses as optional explanation context. The prompt explicitly prevents the explanation turn from changing or reassigning those notes.

internal/ai/explanation.go

parse.goParse and validate bounded agent case output +112/-1

Parse and validate bounded agent case output

â€Ē Parses evidence and ruled-out arrays with item, character, and hypothesis caps. Malformed evidence entries retain their indexes as invalid slots so ruled-out references remain stable.

internal/ai/parse.go

runs.goBind case items through the turn evidence ledger +79/-24

Bind case items through the turn evidence ledger

â€Ē Refactors root-cause and case binding to share one retained-turn match table. Each case item links or fails closed independently, and ruled-out entries survive only when their target is linked.

internal/ai/runs.go

Badge.tsxAdd a muted agent badge tone +5/-1

Add a muted agent badge tone

â€Ē Adds an agent tone for AI-authored evidence role chips. Its muted accent styling intentionally avoids Radar severity colors.

packages/k8s-ui/src/components/ui/Badge.tsx

diagnose.tsExpose diagnosis evidence case API types +38/-0

Expose diagnosis evidence case API types

â€Ē Defines frontend contracts for evidence roles, subjects, linked items, and ruled-out hypotheses. Extends Diagnosis while retaining compatibility with backends that omit the new fields.

web/src/api/diagnose.ts

AgentCase.tsxAdd reusable agent role and claim components +62/-0

Add reusable agent role and claim components

â€Ē Introduces role labels, muted role chips, and visually separated one-sentence agent notes. The components consistently distinguish agent framing from Radar content.

web/src/components/diagnose/AgentCase.tsx

InvestigationEvidencePane.tsxRender and order agent framing over Findings evidence +336/-57

Render and order agent framing over Findings evidence

â€Ē Promotes uniquely placed case items into the main evidence collection, orders groups by their strongest role, and renders role chips and claims on current or historical observations. Adds ruled-out navigation while preserving Radar visibility, grouping, and severity semantics.

web/src/components/diagnose/InvestigationEvidencePane.tsx

InvestigationView.tsxResolve cases for current and earlier assessments +31/-3

Resolve cases for current and earlier assessments

â€Ē Resolves the current diagnosis case independently of root-cause provenance and passes it into Findings and assessment sources. Earlier assessment claims remain read-only and do not annotate current cards.

web/src/components/diagnose/InvestigationView.tsx

investigationCase.tsResolve case items to exact evidence observations +314/-0

Resolve case items to exact evidence observations

â€Ē Adds the core resolver that revalidates turn-scoped references and matches optional subjects against projected observations. Unique matches target cards or revision rows; ambiguous matches fall back to assessment source details.

web/src/components/diagnose/investigationCase.ts

parts.tsxShow agent roles and fallback claims in assessment sources +96/-19

Show agent roles and fallback claims in assessment sources

â€Ē Combines legacy root-cause sources with sources cited by agent case items. Displays role chips and source-placed claims, while earlier assessments also show their observation-bound claims read-only.

web/src/components/diagnose/parts.tsx

Refactor (1) +10 / -0
investigationEvidence.tsExpose evidence reference and source argument helpers +10/-0

Expose evidence reference and source argument helpers

â€Ē Exports evidence-reference validation and parsed source arguments for case resolution and broad-result selection logic.

web/src/components/diagnose/investigationEvidence.ts

Tests (2) +1346 / -0
case_test.goTest case parsing, binding, and explanation propagation +233/-0

Test case parsing, binding, and explanation propagation

â€Ē Adds backend coverage for validation limits, positional invalid items, independent unlinking, scope enforcement, ruled-out indexes, healthy assessments, subject copying, and read-only explanation notes.

internal/ai/case_test.go

investigationCase.test.tsxTest observation placement and Findings rendering +1113/-0

Test observation placement and Findings rendering

â€Ē Adds comprehensive frontend coverage for exact subject matching, fan-out ambiguity, revision placement, role ordering, broad-result promotion, metrics selection, ruled-out links, healthy assessments, and legacy compatibility.

web/src/components/diagnose/investigationCase.test.tsx

Documentation (2) +3 / -0
DESIGN.mdDocument the non-severity agent badge tone +1/-0

Document the non-severity agent badge tone

â€Ē Adds the agent badge tone to the design system guidance. It explicitly distinguishes AI framing from Radar-authored severity and finding colors.

DESIGN.md

mcp.mdDocument the investigation case contract and safety boundary +2/-0

Document the investigation case contract and safety boundary

â€Ē Describes the new evidence and ruled-out fields, server-side ledger binding, frontend placement behavior, and hosted-backend compatibility. Clarifies that agent roles may order and annotate evidence but cannot alter Radar cards.

docs/mcp.md

@qodo-code-review

qodo-code-review Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Evidence notes can miss their cards 🐞 Bug ≡ Correctness
Description
observationSubjectIdentity converts every missing namespace in a stated reference to "",
including network and relationship references whose resource reference leaves namespace optional.
When a case subject supplies a namespace for an unknown namespaced kind, matching rejects it and
leaves the note at the source rather than the observation.
Code

web/src/components/diagnose/investigationCase.ts[R227-231]

+    return {
+      kind: stated.kind,
+      group: stated.group ?? (groupKnown ? "" : undefined),
+      namespace: stated.namespace ?? "",
+      name: stated.name,
Relevance

●●● Strong

Recent matching fixes support preserving wildcard semantics for producer-unstated discriminators.

PR-#1684

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new matching contract says producer-unstated discriminators are wildcards, but the new identity
conversion represents an absent namespace as a known empty namespace. Existing evidence validation
intentionally permits omitted namespaces for unknown resource kinds, and network and relationship
observations pass those references through unchanged.

web/src/components/diagnose/investigationCase.ts[213-231]
web/src/components/diagnose/investigationCase.ts[299-304]
web/src/components/diagnose/investigationEvidence.ts[565-607]
web/src/components/diagnose/investigationEvidence.ts[650-653]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The resolver treats an omitted namespace as known cluster scope for every stated resource reference. Some producers expose references whose namespace is optional and whose scope cannot be inferred, so a case subject that supplies the actual namespace fails exact placement instead of treating the missing producer dimension as a wildcard.

## Fix Focus Areas
- web/src/components/diagnose/investigationCase.ts[213-231]
- web/src/components/diagnose/investigationEvidence.ts[565-607]
- web/src/components/diagnose/investigationEvidence.ts[650-653]

## Recommended Fix
Set an empty namespace only when the observation producer establishes cluster scope. Preserve `undefined` for references with an unstated or scope-ambiguous namespace, including pass-through network and relationship references, so the existing matcher applies its wildcard behavior. Add cases for an unknown namespaced resource with an omitted producer namespace and a namespace-bearing case subject.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Change claims attach to wrong resource ✗ Dismissed 🐞 Bug ≡ Correctness
Description
observationSubjectIdentity returns a changes identity without group, so
observationMatchesSubject treats a supplied group as a wildcard. When a diagnosis bundle's changes
concern one API group and a case item names a same-kind, same-name resource in another group, the
note pins to that changes card.
Code

web/src/components/diagnose/investigationCase.ts[R240-245]

+  if (data.type === "changes" && data.subject?.kind) {
+    return {
+      kind: data.subject.kind,
+      namespace: data.subject.namespace,
+      name: data.subject.name,
+    };
Relevance

●●● Strong

Resource identity matching should preserve group discriminators to prevent cross-group attachment.

PR-#1684

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new resolver makes producer-omitted fields wildcards. Changes subjects currently retain kind,
namespace, and name only, despite diagnose changes being derived from a concrete resource, so a
supplied group cannot participate in identity comparison.

web/src/components/diagnose/investigationCase.ts[240-245]
web/src/components/diagnose/investigationCase.ts[292-296]
web/src/components/diagnose/investigationEvidence.ts[2815-2837]
web/src/components/diagnose/investigationEvidence.ts[2050-2059]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Changes observations discard their API group before case-subject matching. A case item with a conflicting group can therefore match and annotate a changes card for a different resource identity.

## Fix Focus Areas
- web/src/components/diagnose/investigationCase.ts[240-245]
- web/src/components/diagnose/investigationEvidence.ts[401-407]
- web/src/components/diagnose/investigationEvidence.ts[2050-2059]

## Recommended Fix
Carry the known API group into changes observation subjects when producing the projection, and return that group from `observationSubjectIdentity`. Keep it undefined only when the producing call genuinely did not establish a group, so the existing wildcard rule applies only in that case. Add coverage for a conflicting group on a same-name changes subject.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Integrators expect case fields that are omitted ✓ Resolved 🐞 Bug ≡ Correctness
Description
docs/mcp.md says the server emits evidence and ruledOut on every diagnosis, but both fields
use omitempty and bindCase leaves them nil when the agent supplied no evidence items. This
occurs for healthy and inconclusive assessments with no case content, so consumers following this
documentation can require fields that the local server legitimately omits.
Code

docs/mcp.md[212]

+The local investigation's final JSON also carries the agent's case: `evidence` (up to 8 items of `ref`, `role`, one-sentence `claim`, optional `subject`) and `ruled_out` (up to 5 hypotheses pointing at an evidence item by index). The server binds each item's ref against the turn ledger exactly like `root_cause_evidence_refs`, drops a failing item on its own as `unlinked`, and emits server-authored `evidence` / `ruledOut` on the diagnosis for every assessment, healthy and inconclusive included. Findings uses roles only to order cards and to attach an "Agent" note; no role can hide, collapse, or recolor a card, and a claim that cannot be pinned to exactly one observation shows beside its source in Assessment details instead. A hosted backend that sends no `evidence` renders exactly as before.
Relevance

●●● Strong

Documentation contradicts conditional omitempty serialization and should accurately describe absent
fields.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The API tags make both fields conditional, and the binder explicitly returns with both fields nil if
no evidence items were parsed; therefore not every diagnosis contains these JSON properties.

internal/ai/diagnoser.go[291-295]
internal/ai/runs.go[1057-1063]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Correct the API documentation so it does not promise that `evidence` and `ruledOut` are serialized for every diagnosis. The server binds case data for every assessment, but the fields are omitted when no bindable case items exist.

## Fix Focus Areas
- docs/mcp.md[212-212]
- internal/ai/diagnoser.go[291-295]
- internal/ai/runs.go[1057-1063]

## Recommended Fix
Replace the unconditional emission wording with language that distinguishes binding from serialization: case binding runs for healthy and inconclusive assessments too, while server-authored `evidence` and `ruledOut` are included only when there is retained case data. Retain the existing no-evidence frontend compatibility statement.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Malformed notes can label wrong cards ✓ Resolved 🐞 Bug ≡ Correctness
Description
resolveInvestigationCase maps a supplied but invalid subject to undefined, then the !subject
branch treats it as though the agent intentionally omitted all discriminators. When a cited tool
result produced only one observation, malformed fields such as an invalid stream or missing name
still place the claim and role on that card instead of leaving them beside the source.
Code

web/src/components/diagnose/investigationCase.ts[R128-129]

+            (!subject ||
+              observationMatchesSubject(group, observation, subject)),
Relevance

●●● Strong

Malformed input must not become an omitted subject and incorrectly attach claims to cards.

PR-#1684

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The validator returns undefined for missing required fields, invalid optional field types, and
unsupported streams. The resolver subsequently uses !subject as an unconditional observation match
and promotes any unique candidate to card or revision placement, so malformed and omitted subjects
are indistinguishable.

web/src/components/diagnose/investigationCase.ts[114-142]
web/src/components/diagnose/investigationCase.ts[167-185]
web/src/components/diagnose/investigationCase.test.tsx[989-1007]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A supplied but malformed evidence subject is normalized to `undefined` and then treated like an intentionally omitted subject, allowing singleton results to receive incorrectly placed agent notes.

## Fix Focus Areas
- web/src/components/diagnose/investigationCase.ts[114-142]
- web/src/components/diagnose/investigationCase.ts[167-185]
- web/src/components/diagnose/investigationCase.test.tsx[954-1007]

## Recommended Fix
Track whether the subject field was supplied separately from whether it validated successfully. Preserve subject-less matching only when the field was genuinely omitted; when it was supplied but invalid, keep the item source-placed without generating observation candidates, and add a singleton-source regression test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Agent badges use an unapproved fill ✗ Dismissed 📘 Rule violation ⚙ Maintainability
Description
The new agent entry in TONE sets its background with bg-accent-muted rather than an approved
bg-theme-* utility. Any future enforcement or revision of the four-token background palette must
now account for this separate badge-specific path.
Code

packages/k8s-ui/src/components/ui/Badge.tsx[237]

+  agent:      'bg-accent-muted text-accent-text border-accent/30',
Relevance

●●● Strong

Recent rule-compliance precedent accepts replacing non-theme background utilities.

PR-#1685

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Compliance rule 3036653 restricts background utility classes to four approved theme tokens. The
newly added agent tone instead uses bg-accent-muted.

Rule 3036653: Use theme background tokens instead of hardcoded utility color classes
packages/k8s-ui/src/components/ui/Badge.tsx[237-237]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new agent badge tone uses `bg-accent-muted`, while frontend backgrounds are restricted to the approved theme background utilities.

## Fix Focus Areas
- packages/k8s-ui/src/components/ui/Badge.tsx[237-237]

## Recommended Fix
Replace `bg-accent-muted` with the appropriate approved background utility—`bg-theme-base`, `bg-theme-surface`, `bg-theme-elevated`, or `bg-theme-hover`—while retaining suitable semantic text and border styling.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 44 rules
✅ Cross-repo context — repo relationships
  Explored: repo: skyhook-dev/radar-e2e (sha: e4918cc2)
  Explored: repo: skyhook-dev/radar-hub (sha: 58c726b4)
  Explored: repo: skyhook-dev/radar-hub-web (sha: 81f12388)
Review mode: 🧠 Deep: This is a broad, behavior-heavy cross-stack change spanning agent parsing/binding, evidence provenance, observation matching, UI ordering/rendering, and extensive integration behavior, creating many independent opportunities for subtle defects.

Grey Divider

Tip of the day
ðŸ’Ą Did you know, you can turn these tips off under Display preferences

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread packages/k8s-ui/src/components/ui/Badge.tsx
Comment thread web/src/components/diagnose/investigationCase.ts Outdated
Comment thread docs/mcp.md Outdated
Comment thread web/src/components/diagnose/investigationCase.ts
Comment thread web/src/components/diagnose/investigationCase.ts
Add evidence and ruled_out to the agent's final JSON: up to eight
(ref, role, claim, subject) items and five dropped hypotheses. The server
validates each ref against the turn ledger like root_cause_evidence_refs,
drops a failing item on its own as unlinked, and binds the case for every
assessment including healthy and inconclusive. root_cause_evidence_refs is
unchanged.

Findings places a claim on a card only when (ref, subject) names exactly one
observation of that source; a superseded observation gets the claim on its
revision row; anything else renders beside the source in Assessment details.
Roles only order the main list (cause, symptom, unlabelled, context, demoted)
and add an Agent note; they cannot hide, collapse, or recolor a card. A Ruled
out block lists each dropped hypothesis with a link to the placed
observation. Uncited cards and hosted runs render exactly as before.
The main-list ordering seeded the role rank with the unlabelled rank, so
context and demoted groups never moved below Radar's order. Start from the
first item's rank instead and pin the ordering with a test that places a
demoted Key card last.
Match a stated resource identity on every discriminator both sides supply:
a core resource has a known empty group and a cluster-scoped one a known
empty namespace, so a Knative Service subject no longer lands on the core
Service card. Bind a claim to the card head only for the group's latest
observation; a superseded read keeps its own revision row even when it
displays the same, and bound rows survive display folding. Promote a
broader group for a subject-less item only when the cited call was scoped
to one resource. Open the collapsed collections before revealing a ruled-out
target, dedupe ruled-out entries, list an earlier assessment's placed claims
read-only with their observation, and keep the legacy source-row markup
untouched when no case is present.
An agent stating group "" or namespace "" means core group or cluster
scope; omitempty dropped that on the wire and the frontend read it as a
wildcard, so the subject's group and namespace are pointers. Parse each
ruled_out entry on its own so one malformed entry cannot discard its
siblings. Treat a metrics subject's group as known, open history for a
bound revision row even when it displays like the card, and hide a ruled-out
entry whose card is withheld.
A diagnose bundle captures up to three vitals charts for one resource, so
a subject naming the resource with observation "metrics" matches all of
them and falls to the source row. Let observation carry a category
qualifier (metrics:cpu, metrics:memory, metrics:restarts) that selects the
chart whose identity ends in it, tell the agent about it in the prompt, and
pin the promotion of a diagnose-origin chart into main with a test.
A follow-up answer that cites evidence never reached Findings: the pane
only resolved the current assessment's case, so an answer to "chart this
and cite it" left its chart in the collapsed workload collection. The
newest completed non-apply, non-explanation turn with a bound case or
linked root-cause refs now drives selection and chips; the assessment's
own items and earlier answers list read-only, and an answer shows its
sources under the reply.

Placement also accepts the patterns a live agent used: a log stream named
by the diagnosed workload plus container and stream, container or stream
on a non-log observation when the evidence kind is stated, a category
qualifier ignored on an agent-run query chart, and an observation with no
resource of its own named by its evidence kind.
The healthy-conflict banner points at supporting-tier warning and error
cards, but only Key-tier cards carried a coloured rule, so the evidence the
banner named looked neutral. Supporting cards now get a 2px semantic
warning or error rule; the predicate is unchanged, and a test checks that
every card it fires on is visibly marked.
The sources block under Full analysis is provenance: one row per cited
tool result with its scope and a View source action. Role chips no
longer sit on source rows, notes already shown on evidence cards are
counted rather than repeated, and the notes that have no card are
nested under their source with the observation they refer to.

When the agent placed a less-relevant or rules-out note on every card
that would trigger the healthy-conflict banner, the banner now says the
evidence was explained and names the cards instead of reading as a
contradiction. Any unexplained conflict keeps the amber warning.
The agent marks identifiers with backticks in its one-sentence notes and
the note showed the backticks literally.
â€Ķcase

A follow-up that cites one chart became the pane's case and every note
the initial assessment had placed on other cards disappeared with it,
so the healthy-conflict banner accused a logs card the agent had already
marked less relevant. Card- and revision-placed notes from earlier turns
now carry over unless the live turn addressed the same card; ordering,
the ruled-out list and source-placed notes still belong to one turn.
@nadaverell
nadaverell force-pushed the feat/findings-agent-case branch from d21f278 to fa59e11 Compare September 9, 2026 22:50

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/diagnose/investigationCase.ts
A healthy verdict that contradicts captured adverse evidence raises Radar's
own warning. The agent could switch that warning off: labelling each
conflicting card `demoted` or `rules_out` turned it into a blue "no active
problem" notice. The label needed no explanation text, could be pinned to a
superseded read of the card, and could come from a later answer or an
assessment that had already been replaced.

An explanation now reframes the warning instead of replacing it. It stays
amber, still says the evidence is there, and points at the notes; it never
claims a severity tier Radar did not check. To reframe it at all, a note must
belong to the assessment on screen, carry text, sit on the card's current
state, and not be contradicted by a `cause` or `symptom` note from the same
assessment on the same card. None of these judge whether the agent is right,
which is exactly why the warning stays a warning.

Three neighbouring defects in the same lifecycle:

- A follow-up answer replaced the displayed assessment's evidence resolution,
  so the assessment lost its "does not cite specific Radar evidence"
  qualification and evidence it had cited could fall back into the withheld
  count. A later turn's citations now widen the selection and nothing else;
  provenance stays with whoever authored it.
- Carrying a superseded assessment's notes took coverage of a group per item,
  so only the first note on a group survived. The newest assessment that spoke
  about a group now carries everything it said about it.
- A claim on a card the pane withheld was counted as "on an evidence card" and
  suppressed from the source row, so it rendered in neither place. The receipt
  now follows what actually rendered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0125QDX24TNuod7FKLwTDhvW

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/diagnose/InvestigationView.tsx
â€Ķ a merge

Two defects in the previous commit's own lifecycle handling.

The conflict banner asked whether the assessment's qualifying note survived
into the case the pane renders, and asked by object identity. The pane feeds
the merge freshly resolved copies of every turn, so the assessment's items
arrive equal but not identical: any unrelated follow-up made the banner
conclude its own note had vanished and drop back to the unexplained warning.
Survival is now decided by value, and the merge reuses the assessment's own
resolution rather than resolving it a second time.

Reducing a later turn's citations to group ids also lost the source that cited
them. Selecting a broader group is not enough to show it — the broader gate
needs to find who cited it, and with no source it withheld a card the
follow-up had just cited. The citation now travels with its source, while
provenance markers stay with the assessment that authored them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0125QDX24TNuod7FKLwTDhvW
@nadaverell
nadaverell force-pushed the feat/findings-agent-case branch from 6eeef59 to 8fa77cd Compare September 9, 2026 23:57

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/diagnose/InvestigationView.tsx
Comment thread web/src/components/diagnose/InvestigationView.tsx
nadaverell and others added 3 commits September 10, 2026 03:03
Omitting a subject asks Radar to place the note wherever the cited call
produced. Supplying one that does not validate asked for something specific,
and normalising it to "omitted" put the claim on whichever observation
happened to be the only one that call made. Such an item now stays beside its
source.

docs/mcp.md said the server emits `evidence` and `ruledOut` on every
assessment. Binding runs on every assessment; the fields are `omitempty`, so a
diagnosis with no bindable case carries neither. Consumers are told to treat
both as optional.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0125QDX24TNuod7FKLwTDhvW
The live answer's source receipt was built from the merged case, so it listed
notes the merge had carried from earlier assessments as though that answer had
cited them — the same notes already shown on their cards and again under the
assessment that wrote them. It now reads the live turn's own case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0125QDX24TNuod7FKLwTDhvW
The role travelled with the card title, in the same slot as Radar's own kind
and status badges, while only the sentence beneath it was marked as the
agent's. A reader could take "Secret dev/cloud-provisioner · Cause" for Radar
having established causality. Sparkle, role and sentence now share one row, so
the label is as plainly the agent's reading as the sentence is. A role the
agent gave without a sentence still renders that row rather than vanishing.

A rules-out card names what it excludes. "Rules out" alone made the reader
find the hypothesis in a separate block and match it back by hand; the chip
now reads "Rules out: The Atlas user was deleted", and only for the role that
actually excludes something.

The ruled-out block moves under the Assessment. It answers why this conclusion
over the alternatives, so it belongs with the conclusion rather than after the
evidence list among the withheld counts, where it read as bookkeeping.

A card the agent calls the cause takes a 3px accent rule — but only where
Radar left that slot empty. A cause is often a Secret or a ConfigMap that
Radar has no reason to colour, so it sorted first while every card echoing the
failure below it carried red. Severity still outranks the agent's framing:
where Radar coloured the card, Radar's colour stands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0125QDX24TNuod7FKLwTDhvW

@cursor cursor 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.

Stale Bugbot comment from a previous run.

Comment thread web/src/components/diagnose/InvestigationEvidencePane.tsx
Five roles had no way to say that adverse-looking evidence does not indicate an
active problem. `rules_out` excludes some other hypothesis and `demoted` says a
card is peripheral — both true of a problem that is still happening — so an
agent calling a workload healthy over Radar's contradicting evidence reached
for whichever role was nearest. On a live run it labelled a memory chart
`symptom` while its own sentence used that chart to rule memory out.

`benign` says exactly that and nothing else, and it is now the only role that
lets the healthy-conflict banner reframe. The other guardrails are unchanged:
the note must belong to the assessment on screen, carry text, sit on the card's
current state, and not be contradicted by a cause or symptom note on the same
card.

A rules-out card names the hypothesis it excludes as text beside the chip
rather than inside it — a hypothesis long enough to be useful makes a badge
that swallows the row. It renders only for `rules_out`, where the chip frames
it as rejected: a hypothesis is something the agent DISPROVED, so on a card
labelled anything else it reads as a statement of fact.

The card the agent calls the cause lifts its surface instead of taking a
coloured rule. The left rule is Radar's severity and the ring is the source
you are viewing; both were already spoken for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0125QDX24TNuod7FKLwTDhvW

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4fedf9e. Configure here.

Comment thread internal/ai/diagnoser.go
nadaverell and others added 5 commits September 10, 2026 13:36
`benign` was added to the prompt and to the label table but never to the
parser's accepted set, so every benign item was discarded as unlinked. The chip
would never have appeared and the healthy-conflict banner could never have
reframed — silently, with nothing anywhere saying why. A test now walks every
role the prompt offers and fails if the parser rejects one.

The card's note block was still gated on an item having a claim, so a role the
agent gave without a sentence rendered nothing at all: the note itself learned
to handle that case when the chip moved off the title, but the block around it
did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0125QDX24TNuod7FKLwTDhvW
Two things stopped a healthy verdict from ever being reframed, and both were
invisible.

The prompt named the role but never asked the agent to cite the evidence it
applies to. On a live run the agent annotated the results that looked fine and
never cited the one Radar was warning about, so the warning stood over an
assessment that had, in prose, addressed it. It is now told that Radar checks
its verdict against what was captured, which results count as active-problem
evidence, and that leaving one uncited keeps the warning — which is the right
outcome when it has not explained them.

`observe` then keeps one log stream read through two different calls in
separate groups, so a same-named pod from another workload cannot inherit the
target's relevance by colliding on a display identity. That partition is worth
keeping, but it was also deciding whether the agent had addressed the stream:
the agent explained one card and the conflict was recorded on its twin. The
conflict predicate now treats groups of one kind sharing an identity as the
same observation for that question, and nothing else changes.

Verified end to end against a live EKS workload: the agent cites the restart
and the previous-instance logs, marks both benign with its reasons, and the
banner reframes to "Agent reports no active problem; adverse evidence remains"
— still amber, still naming the evidence, with the coverage limitation intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0125QDX24TNuod7FKLwTDhvW
A sparkle, an accent-toned chip and the words "Agent's note:" all said the
same thing on every annotated card, and the reader paid for it in row width on
the densest surface in the product. The sparkle now sits inside the chip, so a
single badge is the agent's mark and the sentence follows it directly.

The words carried attribution where colour and iconography do not reach, so
they stay as screen-reader text — "Agent:" rather than "Agent's note:",
because a role can arrive with no sentence and nothing may announce a note
that is not there.

The role vocabulary does some of this work by itself: Radar writes
CrashLoopBackOff and 0/1 replicas ready, never Symptom or Rules out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0125QDX24TNuod7FKLwTDhvW
Centring the badge box dropped the label about a pixel below the text beside
it, because padding and border make the box taller than the line; aligning to
the badge's own baseline overshot by the same amount, since that baseline
comes from the sparkle rather than from the label.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0125QDX24TNuod7FKLwTDhvW
Two doc comments still described the earlier rule, where a "less relevant" or
"rules out" note could qualify the healthy-verdict banner. Only benign can.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0125QDX24TNuod7FKLwTDhvW
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.

1 participant