diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 09e9cf924..e641beb7a 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -77,9 +77,9 @@ or historical proof/counting for resolved-only work. Assistant handoffs, approvals, verification, and manual controls. A compact row-owned issue scaffold may translate the same finding, approval, and workflow fields into problem, affected resource, consequence, checked - evidence, next step, and verification state, but it must stay attached to - the active issue row and must not become a separate proof, trust, or status - strip. Because a + evidence, safe workflow step, next step, and verification state, but it must + stay attached to the active issue row and must not become a separate proof, + trust, or status strip. Because a contextual Assistant handoff from that workflow is still a first-party Patrol starter for the same governed journey, it must record content-free workflow prompt activity through the shared marker route with the `pulse_patrol` diff --git a/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts b/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts index 694185a5d..8b6d4fdd2 100644 --- a/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts +++ b/frontend-modern/src/components/AI/__tests__/FindingsPanel.test.ts @@ -996,8 +996,10 @@ describe('aiFindingPresentation', () => { expect(aiFindingPresentationSource).toContain("label: 'Affected'"); expect(aiFindingPresentationSource).toContain("label: 'Why it matters'"); expect(aiFindingPresentationSource).toContain("label: 'What Pulse checked'"); + expect(aiFindingPresentationSource).toContain("label: 'Safe workflow'"); expect(aiFindingPresentationSource).toContain("label: 'Recommended next step'"); expect(aiFindingPresentationSource).toContain("label: 'Verification'"); + expect(aiFindingPresentationSource).toContain('getPatrolFindingWorkflowSummary'); expect(findingsPanelSource).not.toContain('Patrol trust summary'); expect(findingsPanelSource).not.toContain('Patrol proof strip'); }); diff --git a/frontend-modern/src/utils/__tests__/aiFindingWorkType.test.ts b/frontend-modern/src/utils/__tests__/aiFindingWorkType.test.ts index 3354bff6a..a85b20d3a 100644 --- a/frontend-modern/src/utils/__tests__/aiFindingWorkType.test.ts +++ b/frontend-modern/src/utils/__tests__/aiFindingWorkType.test.ts @@ -131,6 +131,11 @@ describe('getPatrolFindingRowScaffold', () => { label: 'What Pulse checked', value: 'CPU stayed above the configured warning threshold during the scheduled Patrol check.', }, + { + id: 'workflow', + label: 'Safe workflow', + value: 'Review evidence, decide the next action, and verify any outcome before closing.', + }, { id: 'next-step', label: 'Recommended next step', @@ -164,6 +169,21 @@ describe('getPatrolFindingRowScaffold', () => { label: 'Recommended next step', value: 'A governed fix is waiting for an approve or reject decision.', }); + expect( + getPatrolFindingRowScaffold( + makeRowScaffoldFinding({ + investigationStatus: 'needs_attention', + investigationOutcome: 'fix_queued', + }), + approvals, + Date.parse('2026-06-30T08:07:00Z'), + )?.items, + ).toContainEqual({ + id: 'workflow', + label: 'Safe workflow', + value: + 'Review evidence first; no change runs until the proposed fix is approved, then Patrol verifies the outcome.', + }); expect( getPatrolFindingRowScaffold( makeRowScaffoldFinding({ @@ -180,6 +200,21 @@ describe('getPatrolFindingRowScaffold', () => { }); }); + it('keeps executed fixes on the verification step before closure', () => { + expect( + getPatrolFindingRowScaffold( + makeRowScaffoldFinding({ + investigationStatus: 'completed', + investigationOutcome: 'fix_executed', + }), + )?.items, + ).toContainEqual({ + id: 'workflow', + label: 'Safe workflow', + value: 'The governed action ran; review follow-up evidence before closing the issue.', + }); + }); + it('does not scaffold non-Patrol, runtime setup, or resolved findings', () => { expect( getPatrolFindingRowScaffold(makeRowScaffoldFinding({ source: 'threshold' })), diff --git a/frontend-modern/src/utils/aiFindingPresentation.ts b/frontend-modern/src/utils/aiFindingPresentation.ts index 666a7df1e..e7a1b0cef 100644 --- a/frontend-modern/src/utils/aiFindingPresentation.ts +++ b/frontend-modern/src/utils/aiFindingPresentation.ts @@ -683,6 +683,7 @@ export type PatrolFindingRowScaffoldItemId = | 'next-step' | 'problem' | 'verification' + | 'workflow' | 'why'; export interface PatrolFindingRowScaffoldItem { @@ -752,6 +753,31 @@ function getPatrolFindingVerificationSummary( return 'No fix has run yet.'; } +function getPatrolFindingWorkflowSummary( + workflow: FindingPatrolWorkflowPresentation | undefined, +): string { + if (!workflow) { + return 'Review evidence, decide the next action, and verify any outcome before closing.'; + } + + switch (workflow.stage) { + case 'approval': + return workflow.label === 'Approve or reject' + ? 'Review evidence first; no change runs until the proposed fix is approved, then Patrol verifies the outcome.' + : 'Recover the queued fix before any action can run, then verify the outcome after a decision.'; + case 'verification': + return 'The governed action ran; review follow-up evidence before closing the issue.'; + case 'attention': + return 'Review the blocked or failed step before approving another change or resolving manually.'; + case 'investigating': + return 'Patrol is explaining the issue and preparing the next decision point.'; + case 'recorded': + return 'Patrol recorded the outcome; use history if you need the completed trail.'; + case 'paused': + return 'Patrol will return this issue to the workflow when the reminder expires.'; + } +} + const getNonEmptyPresentationText = ( value: string | undefined, fallback: string, @@ -827,6 +853,11 @@ export function getPatrolFindingRowScaffold( 'Patrol recorded this from the current check.', ), }, + { + id: 'workflow', + label: 'Safe workflow', + value: getPatrolFindingWorkflowSummary(workflow), + }, { id: 'next-step', label: 'Recommended next step', diff --git a/tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts b/tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts index c566d283f..73a9f05ff 100644 --- a/tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts +++ b/tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts @@ -626,6 +626,7 @@ test.describe("Monitor-first Patrol workbench browser contract", () => { await expect(issueSummary.getByText("Affected")).toBeVisible(); await expect(issueSummary.getByText("Why it matters")).toBeVisible(); await expect(issueSummary.getByText("What Pulse checked")).toBeVisible(); + await expect(issueSummary.getByText("Safe workflow")).toBeVisible(); await expect(issueSummary.getByText("Recommended next step")).toBeVisible(); await expect(issueSummary.getByText("Verification")).toBeVisible(); await expect( @@ -642,6 +643,11 @@ test.describe("Monitor-first Patrol workbench browser contract", () => { "CPU stayed above the configured warning threshold during the scheduled Patrol check.", ), ).toBeVisible(); + await expect( + issueSummary.getByText( + "Review evidence first; no change runs until the proposed fix is approved, then Patrol verifies the outcome.", + ), + ).toBeVisible(); await expect( issueSummary.getByText( "A governed fix is waiting for an approve or reject decision.",