Frontend wedge for the per-resource operator-state feature: an operator
working on a resource can now toggle Intentionally offline and Never
auto-remediate without curling the API. The section lives on the
overview tab next to the action audit history so the "what overrides
has the operator set" and "what actions has Pulse taken" stories read
together.
NeverAutoRemediate is a safety override — flipping it on requires an
explicit confirmation prompt naming what the lock means, while flipping
it back off is permissive (releasing a lock is the recoverable action).
Maintenance windows are surfaced read-only this slice; scheduling lives
in a follow-up that owns the date-picker UX.
Uses createNonSuspendingQuery rather than createResource so the
drawer's parent Suspense boundary does not flicker the page-level
"Loading view..." fallback while operator state is in flight. The save
path preserves any currently-persisted maintenance-window data so this
toggle slice does not clobber the future window-scheduler slice.
Adds a TS API client (getResourceOperatorState /
setResourceOperatorState / clearResourceOperatorState) that mirrors
the canonical Go shape from slice 30, with 404 -> null normalization
on the GET path so callers see "no state" as a clean default.
Third compounding slice on per-resource operator state, completing the
suppression triangle: maintenance window (slice 31), intentionally
offline (slice 32), and now NeverAutoRemediate at the action-broker
boundary. When the operator has set NeverAutoRemediate=true on a
resource, executeCommandWithAudit refuses the dispatch even with a
valid approval and matching plan hash — per-resource operator intent
outranks per-action approval.
The check sits next to the drift refusal in the same audit hot path:
fail-open on store-lookup error (logged but doesn't block), persist a
Failed audit record with `resource_remediation_locked:` prefix on the
ErrorMessage so audit-UI filters and alert rules can branch on the
stable token. Returns ErrResourceRemediationLocked sentinel for
caller-side branching.
Reuses the existing actionAuditStore handle (which is the unified
ResourceStore from slice 29), so no new provider plumbing needed —
the broker already has the data path it needs.
Compounding slice on the per-resource operator-state feature: when an
operator has marked a resource as IntentionallyOffline (via the API
surface from slice 30), new findings against that resource get
auto-dismissed as expected_behavior with the lifecycle event tagged
operator_state_cause=intentionally_offline. Same shape as the
maintenance-window suppression from slice 31 but indefinite — no
scheduled end, no maintenance_end_at metadata.
Restructures the ResourceOperatorStateProvider interface to return a
single ResourceOperatorStateProjection per call rather than a narrow
ActiveMaintenanceWindow method. Adding the second signal would
otherwise have meant a second method and another call per finding;
the projection shape carries every signal in one round-trip and gives
future signals (NeverAutoRemediate, criticality) a stable extension
point.
Maintenance windows take priority over intentionally_offline when
both are active because the time-bounded suppression is more honest
to surface to the operator — they'll see it auto-clear when the
window ends rather than wondering when the indefinite suppression
will lift.
Third slice on the per-resource operator-state feature delivers the
behavioral payoff: when an operator has set a maintenance window on a
resource (via the API surface from slice 30), new findings against
that resource get auto-dismissed as expected_behavior at creation time
rather than firing notifications. The finding still lands in durable
history with a UserNote naming the window and a "dismissed" lifecycle
event tagged operator_state_cause=maintenance_window so the operator
can audit what tripped during the window.
Adds a narrow ResourceOperatorStateProvider interface to internal/ai
so the findings runtime stays free of an internal/unifiedresources
import. The API layer wires an adapter in router.go that projects
unified.ResourceOperatorState through state.IsInMaintenanceAt into
the ActiveMaintenanceWindow shape the findings runtime consumes.
Suppression is opt-in: stores without a provider wired keep the
original new-finding behavior bit-for-bit, so deployments that
haven't adopted the operator-state feature see no behavioral change.
IntentionallyOffline and NeverAutoRemediate land as compounding
slices on the same provider interface.
Second slice on the per-resource operator-state feature: the API
surface that the storage foundation from slice 29 was designed to
support. A frontend, pulse-cli, or Assistant tool call can now read
the operator-set state for a resource, replace it with PUT, or clear
it with DELETE.
Contract decisions worth preserving:
- GET 404s with stable error code operator_state_not_set when no
entry exists, distinct from a 200 with default (all-zero) fields.
- PUT replaces the entire record. URL canonicalId wins over body to
prevent body-manipulation retargeting; server-side setAt/setBy
populate from request time and authenticated identity, ignoring
client values so the audit trail stays honest.
- Validation rejections surface 400 with operator_state_invalid so
frontend can branch on the code without string-matching messages.
- DELETE is idempotent — 204 whether or not an entry was present.
- GET runs under monitoring:read; PUT and DELETE under
monitoring:write because the state modulates Patrol's behavior.
Finding-suppression and action-broker integrations land in subsequent
slices that consume the same ResourceOperatorState shape.
Foundational wedge for the operator-set per-resource state feature
(intentionally offline, never auto-remediate, maintenance windows,
criticality hint). Defines the canonical schema, validation, and
persistence contract; subsequent slices wire in the API surface and the
finding-suppression / action-broker integrations.
ResourceOperatorState carries four narrow operator-intent fields plus
attribution metadata. The shape is intentionally fixed (not a freeform
metadata bag) so consumers have a stable contract to honor.
ValidateResourceOperatorState rejects malformed records with a stable
sentinel (partial maintenance window, end <= start, unknown criticality);
NormalizeResourceOperatorState handles whitespace and case.
Adds GetResourceOperatorState / SetResourceOperatorState /
ClearResourceOperatorState to the ResourceStore interface, implements
both on SQLite (new resource_operator_state table) and Memory. Set is
upsert by canonical_id; Clear is idempotent. Tests cover IsEmpty,
IsInMaintenanceAt window semantics (half-open interval, partial windows
treated as no window), validation rejections, and round-trip
persistence on both stores.
Pulse's primary monitored platform is Proxmox, but the per-command-class
preflight catalog only covered systemd services, Docker containers, and
Kubernetes deployments. Operators approving a Patrol-proposed
qm restart/qm stop/qm shutdown saw the generic preflight without the
operational nuance that distinguishes those verbs (qm stop is a hard halt,
qm shutdown is graceful with a 60s ACPI timeout, etc.).
Adds eight new classes covering qm and pct lifecycle (reboot/restart,
stop, start, shutdown). Each gets hand-authored safety and verification
copy that names the actual semantics — pct stop is destructive vs pct
shutdown's lxc-attach handoff — so the operator sees concrete context at
approval time.
Broker-level VerificationCommandForCommand intentionally does not derive
qm status / pct status from these classes — pulse_control's
verifyGuestAction already runs those checks at the tool layer, so adding
a parallel broker dispatch would double-run. The preflight copy still
names what the tool-layer verification will read.
Operators frequently need to delegate a finding by pasting it into Slack,
email, or a ticket. Currently they have to manually copy each piece —
title, description, impact, recommendation — out of the expanded card.
Adds formatFindingForClipboard helper that produces Markdown mirroring the
seven-question schema's render order (severity + title + resource, then
description, impact, recommendation, plus trust signals). Wires a Copy
summary button between Explain and Discuss in the expanded card, routing
through the shared copyToClipboard helper with success/error notifications.
Investigation evidence and rollback plans are intentionally omitted —
those are conversation context for the Assistant flow, not "share this
finding" context for chat or tickets.
Third wedge of the Patrol page IA reframe. The recency line "Last full
patrol: 3m ago" tells the operator when Pulse last ran but not what it
covered. Adds a coverage signal so the line reads "Last full patrol: 3m
ago — verified 47 resources" — operators see temporal AND coverage state
without scrolling.
Extends PatrolRecencyPresentation with an optional resourcesChecked
populated from PatrolRunRecord.resources_checked on the latest completed
run. Field is intentionally optional (omitted when zero) so a degenerate
zero-coverage run doesn't render "verified 0 resources" as an alarm.
Second wedge of the Patrol page IA reframe. The detailed Trust strip in
PatrolIntelligenceWorkspace stays put, but operators should see "N active,
M regressed, K fixes verified" immediately under the page title without
scrolling into the workspace tabs — the trust loop's own state should be
the first operational signal on the page, not buried under runtime
controls and tab bars.
Reads from the same FindingsTrustSummary block already plumbed through
state.patrolStatus().trust, gated on at least one non-zero signal so
fresh installs render no header strip. Tone classes mirror the existing
detailed strip (amber for regression, emerald for verified fixes) so the
visual language stays consistent between header and workspace views.
The Patrol page header used to read "Continuously verify infrastructure
health, review findings, and control Patrol runtime behavior" — passive
monitoring console framing that hides what Patrol actually does. The Pulse
Intelligence vision is proactive: Patrol investigates the infrastructure,
captures evidence, and proposes safe fixes under operator approval.
Updates the canonical PATROL_PAGE_DESCRIPTION to name that loop directly,
adds a PATROL_PAGE_TITLE_TOOLTIP that reads the same string so hover and
inline can't drift apart, and rewires PatrolIntelligenceHeader to consume
both from the canonical helper instead of carrying an inline copy. First
wedge of the Patrol page IA reframe; structural changes (header trust
signals, tab semantics) follow in subsequent slices.
Slice 22 added the manual Mark resolved button (auto_resolved=false) but the
resolution-reason copy still flattened every closure into "Condition
cleared" or "Issue no longer detected" — the operator couldn't tell from
the timeline whether they had closed the loop themselves or Pulse had
auto-detected the condition clearing.
Threads the existing Finding.AutoResolved flag through unified.UnifiedFinding
(Go), router.go conversions, UnifiedFindingRecord (TS), and the store-level
UnifiedFinding. The frontend resolution-reason helper now reads "Resolved by
you <time>" when autoResolved === false, while keeping Patrol's specific
fix outcomes (fix_verified, fix_executed, resolved) priority because those
describe Pulse's actual remediation rather than mere auto-detection.
The /api/ai/patrol/resolve endpoint already existed server-side, but the
operator surface had no path to it. An operator who fixes an issue
out-of-band had to wait for Pulse's auto-detection to clear the finding,
or work around through the API directly. Adds a TS client (resolveFinding),
a store action (aiIntelligenceStore.resolveFinding), and a Mark resolved
button next to Acknowledge in the expanded controls.
The button is gated to active findings (the server rejects double-resolves
with 404) and styled with an emerald accent so it reads as a positive
closure rather than a destructive action like Dismiss. Re-detection still
flows through the regression path so PreviousResolvedFixSummary captures
operator-driven resolutions in operational memory.
regressionCount is the strongest "this is not a one-off" signal Pulse can
give an operator scanning a list, but until now it lived inside the expanded
card. A triaging operator should be able to see at a glance that a finding
has come back N times, alongside the existing investigation-confidence badge.
Adds an amber "regressed N×" pill in the collapsed row when
regressionCount > 0, sitting next to the confidence badge so trust signals
read as a single cluster. Pill stays absent on fresh detections so ordinary
finding rows stay clean.
If an operator hits "Dismiss: Not an issue" or "Dismiss: Expected" on a
finding that has already regressed twice or more, they may be permanently
suppressing something Pulse keeps re-detecting. The dismiss confirmation
panel now shows a non-blocking amber hint when regressionCount > 1 and the
selected reason is not_an_issue or expected_behavior, nudging them toward
the reminder-bearing will_fix_later path without blocking the dismiss.
The hint never appears for will_fix_later itself (already commitment-
tracking) or for findings with no prior regression, so it stays quiet on
ordinary triage. This is the operator-facing half of "Pulse learns from
dismissal patterns" — a recurring issue should not be silently buried.
Slice 18 made will_fix_later a real operational commitment server-side, but
the new RemindAt field stayed invisible to operators until the reminder fired
a week later. This wires it through the API surface and renders it where the
operator decides and where they later revisit.
UnifiedFinding (Go and TS) and the Patrol Finding TS shape now carry
RemindAt / remind_at; router.go and AddFromAI mirror it like the other
user-feedback fields. FindingsPanel previews "Pulse will stay quiet for 7
days, then surface again on <date>" on the dismiss confirmation panel before
the operator confirms, badges dismissed-as-will_fix_later rows with
"Reminding <date>" in amber, and adds explanatory copy for the other two
dismissal reasons so all three paths feel deliberate rather than
undifferentiated.
Before this change, all three dismissal reasons funnelled through the same
"swallow re-detection at same/lower severity" path in FindingsStore.Add, so
will_fix_later was functionally identical to expected_behavior — Pulse stayed
quiet forever despite the dismiss_finding tool literally telling the LLM
"Pulse Patrol will continue to monitor this issue."
Now will_fix_later sets Finding.RemindAt (default 7 days) at dismissal time.
Once RemindAt has passed, the next re-detection clears the dismissal and emits
a `reminded` lifecycle event so the operator sees their lapsed commitment
instead of a swallowed finding. expected_behavior keeps acknowledged-forever
semantics; not_an_issue keeps Suppressed=true. The LLM tool response now
surfaces the remind-at date so Patrol's conversational explanations stay
aligned with the contract.
The broker's read-after-write verification (ActionVerificationResult on
ExecutionResult) was being persisted by the backend but no operator
surface displayed it. Operators reviewing the action history saw only
"command exit 0" — not "Pulse confirmed the workload service is active
after dispatch."
Adds the TS mirror for ActionVerificationResult on
types/actionAudit.ts and renders it on each audit row in
ResourceActionHistory.tsx when verification.ran=true. The render shows:
- "Verified" or "Verification failed" badge tone
- The verification command Pulse ran (e.g. systemctl is-active 'nginx')
- The captured output verbatim
- An italic note when the broker recorded one (dispatch failure,
non-zero exit code)
Tone is emerald for verified, amber for failed — matching the trust
palette used for the confidence badge on the patrol findings panel.
When verification.ran=false (no derivable check, or feature disabled
for the action class) nothing renders, so operators do not see
fabricated "verified" claims for actions where Pulse cannot read back.
Verification artifacts:
- ResourceActionHistory.verification.test.ts: source-text test pinning
the verification render wiring on ResourceActionHistory.tsx.
- ResourceDetailDrawer.history.test.tsx: extends the existing
contract-style assertions to pin verification rendering on the
detail drawer's audit rows.
- actionAudit.test.ts: round-trips a verification block through the
API client to pin the TS type mirror.
Adds new Completion Obligation #23 to unified-resources contract
pinning the canonical TS mirror location and the canonical render
component, and a parallel api-contracts paragraph pinning the
verification block on the action audit response.
Closes the read-after-write piece of the user's act-with-control loop:
"Act with control: ask for approval, execute through Pulse tools, then
verify." Until now Pulse authored verification narrative copy at
approval time but never actually executed the check.
Adds ActionVerificationResult to internal/unifiedresources/actions.go
and embeds it as ExecutionResult.Verification so the existing
result_json column persists the outcome without a schema migration.
The struct records: did the check run, what command did Pulse send,
what came back, did it succeed, when, and any failure note.
Adds VerificationCommandForCommand in tools_control.go that derives
the per-class read-after-write check used by the broker:
- service-restart / service-start / service-reload / service-stop
→ systemctl is-active <unit>
Container classes (container-restart, container-stop) are
intentionally deferred to pulse_docker's existing tool-level
docker inspect verification — adding a broker-level dispatch would
double-run the same check.
executeCommandWithAudit now runs the derived verification command via
the same agent path immediately after a successful dispatch, captures
output and exit code, and writes the result onto
ExecutionResult.Verification before recordActionExecutionResult
persists. If the check returns non-zero, Verification.Success=false
with a Note explaining the exit code so the audit history honestly
shows that Pulse ran the action but couldn't confirm it took.
Verification is best-effort and class-scoped: unknown command shapes
leave Verification nil rather than fabricating a verified=true entry,
matching the no-fabrication boundary from Impact / preflight authoring.
Tests cover:
- VerificationCommandForCommand derives the right check per class,
shell-escapes single quotes in unit names, and returns "", false
for docker (deferred) and unknown commands.
- TestExecuteCommandWithAuditRunsClassDerivedVerificationAfterDispatch
- TestExecuteCommandWithAuditMarksVerificationFailedWhenReadbackDoesNotConfirm
- TestExecuteCommandWithAuditSkipsVerificationForUnclassifiedCommands
Updates ai-runtime contract with the read-after-write rule and
container-class deferral. Adds Completion Obligation #21 to
unified-resources pinning ExecutionResult.Verification shape and the
no-fabrication boundary. Extends TestActionExecutionContractStaysAPIOwned
to pin ActionVerificationResult struct and the Verification field on
ExecutionResult.
Operators about to approve a Pulse-driven action need to know what the
command actually touches before saying yes. Until now the preflight
shown at approval time was the same generic boilerplate for every
action: "approval is scoped, hash must match, etc." — useful safety
posture but no operational specifics. The dry-run summary said the
same thing for 99% of actions.
Adds classifyApprovalCommand and approvalCommandClassPreflightAdditions
in tools_control.go that bucket common Pulse remediation actions and
return hand-authored safety + verification additions per class:
- service-restart (systemctl restart, service restart)
- service-stop (systemctl stop, service stop)
- service-start (systemctl start, service start)
- service-reload (systemctl reload)
- container-restart (docker / podman restart)
- container-stop (docker / podman stop)
- k8s-rollout-restart (kubectl rollout restart)
The additions name concrete operational facts the operator needs:
- For service-restart: "Service will be briefly unavailable; no other
unit dependencies altered" plus verification "Read back
systemctl is-active <unit>" and "tail journal for crash patterns".
- For container-restart: "Container will be briefly unavailable;
image and volume mounts unchanged" plus "Read back via docker
inspect" verification.
- For k8s-rollout-restart: "Pods rolled in waves per deployment
strategy; PodDisruptionBudget continues to apply" plus "watch
kubectl rollout status" verification.
approvalPreflight now appends class-specific safety and verification
content onto the existing default content rather than replacing it,
so the broker's structural safety posture (org scope, hash match,
single-use approval) remains visible alongside the operational copy.
Unknown command classes return nil/nil — no fabricated padding for
commands the bucket does not recognize. The default preflight stands
on its own, matching the no-fabrication rule that runs through the
rest of the trust-record arc.
Tests cover: each known class returns non-empty additions with the
expected operational tokens; unknown commands return nil; the
end-to-end approvalPreflight merge surfaces both default and
class-specific safety/verification entries. ai-runtime contract
pinned with the per-class enrichment rule and the no-fabrication
boundary for unknown classes.
The action audit log is plaintext SQL. Operators sometimes paste
secrets into a natural-language `reason` field ("rotate the key
sk-abc123 because it leaked"), and command output sometimes echoes
tokens. Both flows previously persisted unredacted, leaving the
audit history as a leaked-credential surface.
Adds RedactAuditText / RedactAuditRecord in a new
internal/unifiedresources/audit_redaction.go with a curated regex set
that targets the credential shapes most likely to be real secrets:
- URL with embedded basic-auth credentials (https://user:pass@host)
- Authorization: Bearer <token> and x-api-key: <token> headers
- Query-string secret params (?api_key=, &token=, &access_token=)
- JSON-style secret fields ("api_key": "...", "password": "...")
- Env-style or CLI-style secret assignments (PASSWORD=, api_key=)
- OpenAI/Anthropic-style API keys (sk-...)
The set is intentionally narrower than the patrol-failure redactor in
internal/ai/patrol_runtime_failure.go: it does NOT strip arbitrary
URLs, because operators legitimately reference runbooks, ticket
links, and GitHub issues in audit reasons. Only patterns very likely
to be real secrets are touched.
Wired at the persistence boundary (top of each store method) so all
record paths get redaction uniformly:
- SQLiteResourceStore.RecordActionAudit
- SQLiteResourceStore.RecordActionExecutionStart
- SQLiteResourceStore.RecordActionExecutionResult
- MemoryStore.RecordActionAudit
- MemoryStore.RecordActionExecutionStart
- MemoryStore.RecordActionExecutionResult
Plan, Approvals, and identity fields are left alone — they are
produced by Pulse, not operators or external command output, so they
do not need redaction (and changing them would break PlanHash drift
detection).
Verification artifacts:
- audit_redaction_test.go: pattern-by-pattern coverage plus public-URL
passthrough and empty-string passthrough; full RedactAuditRecord
shape test (Reason + Params string values + Result output redacted;
non-string Params and Plan untouched).
- code_standards_test.go: pins RedactAuditRecord call sites in
store.go and helper signatures in audit_redaction.go so future
refactors cannot silently bypass redaction.
- registry_test.go: cross-cutting integration test that exercises the
registry-store boundary (MemoryStore.RecordActionAudit) and asserts
redaction is applied while Plan fields stay untouched.
Adds new Completion Obligation #20 to unified-resources contract
pinning the redaction-at-persistence-boundary rule and the
no-touch-Plan-fields invariant.
Slice 13 added the drift refusal path but only logged at WARN level —
the audit history showed nothing, so an operator reviewing the action
trail could not see "Pulse caught this drift attempt." Now the drift
branch writes a Failed audit record with Result.ErrorMessage prefixed
"plan_drift:" and dispatches a Failed lifecycle event before returning
ErrActionPlanDrift.
The record carries the same Request, Plan, and Approvals snapshots that
a normal audit record would, so the operator-facing audit row shows
exactly what was attempted and what was approved. The "plan_drift:"
prefix is a stable token for downstream surfaces (audit UI filters,
alert rules) to distinguish drift refusals from generic execution
failures.
Extends TestExecuteCommandWithAuditRefusesPayloadDriftAgainstApprovedPlan
to assert the audit record exists with State=Failed and the plan_drift
error message after refusal. Updates the code-standards snippet check
for ErrActionPlanDrift to match gofmt's actual alignment in actions.go.
ai-runtime contract pinned with the audit-record-on-drift rule.
PlanHash existed on ActionPlan as the contract for "the operator
approved exactly this (command, target, reason) combination" but the
broker never validated it: at execute time the freshly-recomputed hash
was overwritten by the approved plan's hash via mergeApprovedActionPlan
without comparison. A drifted payload (LLM re-emits with different
command, agent ID changes between approval and execute, malicious
injection) would run under a stale approval.
Fix: at the dispatch boundary in executeCommandWithAudit, recompute the
approval-equivalent hash from the actual payload using approvalPlanHash
(same function used at approval-creation time, so direct comparison is
meaningful), compare to plan.PlanHash, and refuse with a new
ErrActionPlanDrift error when they differ. The drift refusal also logs
at WARN level with action_id, approval_id, and both hashes so audit
review can see when drift was caught.
When approvedHash is empty (older approval records or contract paths
that did not author one), validation is skipped and existing behavior
is preserved.
Adds two tests:
- TestExecuteCommandWithAuditRefusesPayloadDriftAgainstApprovedPlan:
approval is for "systemctl restart workload"; payload at execute
time is "rm -rf /var/log/pulse"; expects ErrActionPlanDrift and no
agent dispatch.
- TestExecuteCommandWithAuditAllowsMatchingPlanHash: same payload
matches the approved hash; expects normal dispatch.
Updates two pre-existing tests that used stub PlanHash strings:
- TestExecuteCommandWithDeniedApprovalDoesNotDispatch: was testing the
denial path with PlanHash:"sha256:test". Now uses the real approval-
equivalent hash so denial fires (not drift) and the test still
isolates the denial behavior.
- ControlledConsumesApprovedCommandWithResolvedRoutingTarget: same fix
pattern.
Extends TestActionExecutionContractStaysAPIOwned in code_standards
test to pin ErrActionPlanDrift's existence in actions.go so future
refactors cannot silently downgrade drift into a generic error kind.
Native action path (executeNativeActionWithAudit) is left for a
follow-up: it has the same drift gap but uses actionPlanHashForParams
which is shaped differently from approvalPlanHash, so a sound fix
needs a coherent canonical hash function rather than just adding the
check. Contracts pinned in ai-runtime and unified-resources (new
Completion Obligation #20).
The previousResolvedFixSummary captured at regression time (slice 7) is
already woven into the Assistant chat context as a "Previous Resolved
Fix" line, but the operator cannot see it without opening Assistant.
Render it directly on the expanded finding card so "what worked last
time" is visible inline alongside Description / Impact / Recommendation,
with emerald accent styling that reads as a positive operational
memory cue rather than another alert.
TS plumbing that was missing on the frontend after slice 7's backend
work:
- previous_resolved_fix_summary on UnifiedFindingRecord and PatrolFinding
in api/ai.ts and api/patrol.ts
- previousResolvedFixSummary on the store-level UnifiedFinding
- normalizeUnifiedFindingRecord and normalizePatrolFindingRecord copy
the field through
FindingsPanel renders the summary only when populated; findings
without a captured prior fix continue to show no extra row, matching
the no-fabrication rule.
Adds verification artifacts:
- Source-text test pinning the new render
- frontend api round-trip test for the field
- Boundary test asserting the patrol-context model does not absorb the
per-finding memory shell into the per-record investigation
presentation
Updates the api-contracts, ai-runtime, and patrol-intelligence
contracts to pin the TS mirror, the FindingsPanel render surface, and
the per-finding-shell vs per-record-presentation boundary.
Wire FindingsStore.GetTrustSummary through PatrolService and the
patrol-status API into the Patrol page so the operator can scan
"is Pulse useful?" at a glance. Adds a small Trust strip above the
Findings/Runs tab bar that renders compact signals: fixes verified,
auto-resolved, dismissed-as-noise, dismissed-as-expected, currently
active, and regressed-at-least-once. The strip is hidden when every
signal is zero so a fresh install sees no empty pill.
Plumbing:
- PatrolService.GetFindingsTrustSummary accessor (delegates to the
store-level method shipped in the prior slice)
- PatrolStatusResponse carries Trust *FindingsTrustSummary; populated
from the active patrol service, omitted when no service is available
(snapshot semantics, not lifetime totals)
- TS FindingsTrustSummary mirror in api/patrol.ts and a trust field on
PatrolStatus
- PatrolIntelligenceWorkspace reads state.patrolStatus()?.trust and
conditionally renders the strip
Verification artifacts:
- internal/ai/patrol_test.go: TestPatrolService_GetFindingsTrustSummary
- internal/api/contract_test.go: TestContract_PatrolStatusTrustJSONSnapshot
pinning the canonical wire shape
- frontend-modern/src/api/__tests__/patrol.test.ts: round-trip test for
the trust block on the patrol-status response
- frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceWorkspace.test.ts:
source-text test pinning state.patrolStatus()?.trust read,
aria-label, and field names so future strip additions go through
the FindingsTrustSummary contract first.
- frontend-modern/src/features/patrol/__tests__/patrolInvestigationContextModel.test.ts:
pins that the per-finding context model does not synthesize impact
from trust counters; trust is an aggregate operator-page concern,
not a per-finding text source.
Updates the api-contracts, ai-runtime, patrol-intelligence,
agent-lifecycle, frontend-primitives, and storage-recovery contracts
to pin the trust block's shape, the strip's contract-first rule, and
the scope boundary (trust counters are advisory operator context, not
enrollment/storage/recovery action authority).
Adds FindingsTrustSummary struct and GetTrustSummary() method that
walks the in-memory findings store and returns a snapshot of how
currently-tracked findings have resolved: tracked, currently-active,
resolved (auto-resolved subset), fix-verified vs fix-failed, dismissed
broken out by reason (noise / expected / later), suppressed, and
regressed-at-least-once.
This is the data layer for the user's "trust metrics" arc — concrete
counts an operator can use to answer "do I trust Patrol?" Operators see
fix-verified and dismissed-as-noise grow over time as Patrol's analysis
gets sharper. The summary is intentionally a snapshot, not lifetime
totals; once findings are cleaned up they no longer contribute, so the
AutoResolved counter (which includes both Resolve(auto=true) and
UpdateInvestigationOutcome(fix_verified) paths) is a current-state
distribution, not a historical aggregate. The struct doc string is
explicit about this so downstream surfaces do not misframe it.
Adds a unit test covering each bucket (active, auto-resolved,
fix-verified, fix-failed, dismissed-as-noise, dismissed-as-expected,
regressed) with a fixture that exercises the real lifecycle methods
rather than mutating the store directly. Updates the ai-runtime
contract to pin the snapshot semantics and the AutoResolved-path
union.
Adds a contextual "Explain" entry point next to the existing
"Discuss with Assistant" button on every finding card. The new button
opens Assistant with the same handoff context (investigation record,
operational memory, pending approval, proposed fix) but seeds a
different leading sentence: "Explain this Patrol finding... Walk me
through what we know, why it matters for the affected workloads, how
confident the analysis is, and whether the recommended action is the
right next step." This routes the LLM toward an explanatory framing
rather than open-ended discussion, matching the user's vision of
specific contextual entry points instead of a single generic chat
button.
Plumbing changes:
- New PatrolAssistantFindingIntent type ('discuss' | 'explain')
- Optional intent on PatrolAssistantFindingPromptInput and
PatrolAssistantFindingHandoffInput
- buildPatrolAssistantFindingPrompt switches the leading sentence on
intent; downstream context attachment is identical so trust signals
(impact, confidence, previous resolved fix, etc.) flow through both
paths uniformly.
- FindingsPanel extracts the shared handoff into a small
openFindingInAssistant(finding, intent) helper used by both
handleDiscussWithAssistant and the new handleExplainFinding.
Adds two tests: explain-intent prompt has explanation framing while
discuss-intent keeps the existing wording, and the FindingsPanel
source-text test pins the new button + handler wiring. Updates the
patrol-intelligence, frontend-primitives, and api-contracts contracts
to pin the contextual-intent rule and the uniform-context-attachment
invariant.
The seven-question schema's confidence answer was previously buried in
the expanded investigation section. Surface it in the collapsed row
next to the investigation outcome badge so operators can scan trust
without expanding every finding.
Adds getInvestigationConfidenceBadgeClasses helper in
aiFindingPresentation.ts with a small palette: high is reassuringly
emphasized (emerald), medium is neutral, low is a soft amber so the
operator notices when the trust signal is weak. The badge renders only
when finding.investigationRecord?.confidence is set; findings without
investigation records continue to show no confidence badge, which is
the correct semantic (we have no recorded confidence to display).
Adds a source-text test that pins the badge wiring against future
refactors. Updates the patrol-intelligence contract to pin the
collapsed-row confidence badge surface and the no-fabrication rule.
Capture the prior InvestigationRecord.ProposedFix.Description into a
new Finding.PreviousResolvedFixSummary field at regression time, before
the InvestigationRecord is cleared. Without this capture the next
investigation starts from blank context whenever a finding regresses,
and operational memory of "what worked last time" is lost.
The summary propagates through:
- FindingsStore.Add regression branch (capture before clear)
- Finding.MarshalJSON / UnmarshalJSON (wire shape)
- Both Finding to UnifiedFinding conversion sites in router.go
- UnifiedFinding (struct + JSON shadow + Marshal/Unmarshal)
- UnifiedStore.AddFromAI update branch (non-empty overwrite)
- Assistant chat context as a "Previous Resolved Fix" line so the LLM
sees what worked previously rather than blank-slate diagnosing each
regression.
Adds a unit test that walks the full lifecycle (detect, resolve via
UpdateInvestigationRecord + ResolveWithReason, re-detect) and asserts
PreviousResolvedFixSummary is preserved while InvestigationRecord is
cleared, plus two chat-context tests covering the surfaces-when-set and
omits-when-empty cases. Adds a contract test pinning the canonical
"previous_resolved_fix_summary" JSON key. Updates the api-contracts,
ai-runtime, and the dependent agent-lifecycle, performance-and-
scalability, and storage-recovery contracts to pin the operational
memory propagation rule and its scope boundary.
Extend the patrol_report_finding LLM tool schema with an optional
impact parameter and propagate it through PatrolFindingInput,
patrolFindingCreatorAdapter.CreateFinding, and into Finding.Impact so
LLM-authored Patrol findings carry consequence-if-ignored copy at
detection time alongside the curated catalogs already in place for
runtime failures and threshold alerts.
Updates the patrol system prompt with an "Authoring Impact" section
that instructs the LLM to write concrete operational consequences
(named workloads, jobs, recovery windows) rather than echoing severity
or category, and to leave impact empty rather than fabricate a
consequence when one is genuinely unknown. The eval-pass prompt gets a
shorter version of the same guidance.
Adds two unit tests: one covering an authored impact passing through
to PatrolFindingInput.Impact, and one covering the omitted-impact case
where the contract is honored verbatim with no synthesized default.
Updates the ai-runtime contract to pin the tool-schema authoring rule
and the no-fabrication invariant.
When a Patrol finding has a generated remediation plan, the plan's
per-step Rollback strings are now aggregated into the durable
InvestigationRecord.Rollback field at record-build time. Previously
rollback metadata existed only nested inside RemediationStep.Rollback,
forcing operator-facing surfaces and Assistant prompt context to walk
into per-step payload to answer "what's the undo for the proposed fix?"
Adds AggregatePlanRollbackSteps in internal/ai/investigation_records.go
which deduplicates non-empty rollback strings from a plan's steps.
Wires the aggregation into the patrol_findings.go investigation-record
build site so rollback flows automatically when the PatrolService has a
RemediationEngine attached and the finding has an active plan.
Adds three sub-tests covering nil plan, empty-rollback-step skip, and
duplicate-rollback dedup. Updates the ai-runtime contract to pin the
RemediationPlan to InvestigationRecord rollback aggregation rule.
Extend Impact authoring to threshold alerts: convertAlertToFinding
calls a new generateImpact(alertType) that returns hand-authored
consequence-if-ignored copy keyed on alert type (cpu, memory, disk,
storage, temperature, offline, poweredOff, plus their aliases). Unknown
alert types return an empty string rather than synthesizing generic
text, matching the contract that impact must be authored, not invented.
Fix two propagation gaps in the unified store update paths:
- AddFromAlert update branch backfills Impact on existing findings that
pre-date the Impact contract (description and recommendation
intentionally remain non-refreshed so the addition does not change
historical alert wording).
- AddFromAI update branch overwrites existing.Impact when the incoming
finding has impact set, the same pattern already used for
description and recommendation, so re-detected AI patrol findings
carry freshly-classified impact text into the unified store.
Adds unit tests for generateImpact (one per alert type plus a
returns-empty-for-unknown case) and for the AddFromAI Impact
propagation path. Updates the ai-runtime contract to pin the threshold
alert impact catalog and the unified-store propagation rules.
Carry the Finding.Impact text added in the previous slice through the
Finding to UnifiedFinding boundary and onto the FindingsPanel surface
so the runtime-failure consequence-if-ignored copy is visible to the
operator. Add Impact to the UnifiedFinding struct, JSON snapshot, and
both Marshal/Unmarshal mirrors; copy f.Impact into both Finding to
UnifiedFinding conversion sites in router.go; mirror impact in the TS
UnifiedFindingRecord and Finding API types and the aiIntelligence
store normalizers; render an Impact line between Description and
Recommendation in FindingsPanel.
Also fix the FindingsStore.Add dedup-merge path so re-detected findings
overwrite existing.Impact alongside Description and Recommendation
rather than preserving the stale empty value left by an older binary.
Without this fix, a freshly-classified runtime failure with new Impact
text would be merged onto the persisted finding but the Impact field
would be silently dropped.
Verified end-to-end against the live runtime: triggered a Patrol run,
watched the runtime-failure finding regenerate, confirmed the
operator-visible card now renders "Impact: While Patrol cannot
analyze..." between Description and Recommendation. Updates the
api-contracts, ai-runtime, patrol-intelligence, and the dependent
agent-lifecycle, performance-and-scalability, and storage-recovery
contracts to pin the propagation rule and the dedup-merge invariant.
Add Impact (consequence-if-ignored) to the Finding struct so
detection-time analysis can author operator-facing impact text alongside
the existing description and recommendation, and propagate that field
into the durable aicontracts.InvestigationRecord through
BuildFindingInvestigationRecord. Wire the Patrol runtime-failure
classification path (patrolRuntimeFailureFromError) to populate a
shared impact statement covering every failure cause: while Patrol
cannot analyze, alerts continue to fire without evidence or recommended
actions, and AI Intelligence summaries cannot refresh. The text is
constant across causes because the operational consequence of a
non-running Patrol does not change with the cause; only the
recommendation does. Updates the ai-runtime contract to pin the
detection-time impact authoring rule and forbid model-side
severity/category-derived impact synthesis.
Promote the seven-field investigation-record shape so Patrol findings
can carry consequence-if-ignored context and a record-level rollback
plan alongside the existing verification array. The shared
aicontracts.InvestigationRecord struct gains top-level Impact and
Rollback fields with matching TS mirrors, normalizes Rollback to an
empty slice, and the Patrol-owned investigation surface renders an
explicit "Impact not assessed" / "Rollback not specified" placeholder
so the operator-visible gap is conspicuous to both the operator and
Assistant when Patrol has not populated them. Backend default leaves
both empty rather than fabricating analysis from severity/category.
Also closes the existing Trigger.cause drift between Go and TS so
frontend handoff context preserves backend-attributed failure cause,
and updates the api-contracts, ai-runtime, frontend-primitives, and
patrol-intelligence subsystem contracts to pin the new shape.