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.