Add Patrol safe workflow row guidance

This commit is contained in:
rcourtman 2026-06-30 16:47:55 +01:00
parent 477d549efd
commit f0c81de00e
5 changed files with 77 additions and 3 deletions

View file

@ -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`

View file

@ -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');
});

View file

@ -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' })),

View file

@ -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',

View file

@ -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.",