paperclip/ui/storybook/stories/successful-run-handoff.stories.tsx
Dotta 454edfe81e
Add recovery handoff system notices (#5289)
## Thinking Path

> - Paperclip orchestrates AI agents for zero-human companies.
> - Agent runs can end productively while the source issue still lacks a
durable final disposition.
> - That leaves the control plane unsure whether to resume, escalate, or
close the work.
> - Issue comments also need a presentation contract so system-authored
recovery notices can render as first-class thread messages without
overloading normal comments.
> - This pull request adds successful-run handoff recovery, comment
presentation metadata, and system notice rendering.
> - The benefit is stricter task liveness with clearer operator-facing
recovery state.

## What Changed

- Added successful-run handoff decisions, wake payloads, escalation
behavior, and recovery tests.
- Added issue comment presentation metadata with migration
`0078_white_darwin.sql` and shared/server/company portability support.
- Rendered recovery/system notices in issue chat with dedicated UI
components, fixtures, tests, and storybook/lab coverage.
- Included the current recovery model-profile hint patch so automatic
recovery follow-ups use the cheap profile.

## Verification

- `pnpm install --frozen-lockfile`
- `pnpm exec vitest run
server/src/services/recovery/successful-run-handoff.test.ts
ui/src/components/SystemNotice.test.tsx
ui/src/lib/system-notice-comment.test.ts
ui/src/components/IssueChatThreadSystemNotice.test.tsx`

## Risks

- Migration-bearing PR: merge this before any other branch that might
later add a migration.
- The branch touches both recovery services and issue-thread rendering,
so review should pay attention to recovery wake idempotency and comment
metadata compatibility.

## Model Used

- OpenAI GPT-5 Codex via Paperclip `codex_local` adapter, with
shell/git/GitHub CLI tool use.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-05-06 06:05:58 -05:00

196 lines
7.4 KiB
TypeScript

import type { Meta, StoryObj } from "@storybook/react-vite";
import type { ReactNode } from "react";
import { AlertTriangle } from "lucide-react";
import { IssueBlockedNotice } from "@/components/IssueBlockedNotice";
import { KanbanBoard } from "@/components/KanbanBoard";
import { SuccessfulRunHandoffCommentCallout } from "@/components/IssueChatThread";
import { Identity } from "@/components/Identity";
import { cn, relativeTime } from "@/lib/utils";
import { formatIssueActivityAction } from "@/lib/activity-format";
import {
SUCCESSFUL_RUN_HANDOFF_ESCALATED_ACTION,
SUCCESSFUL_RUN_HANDOFF_REQUIRED_ACTION,
SUCCESSFUL_RUN_HANDOFF_RESOLVED_ACTION,
successfulRunHandoffActivityTone,
} from "@/lib/successful-run-handoff";
import { createIssue, storybookAgents } from "../fixtures/paperclipData";
function ActivityExample({ action }: { action: string }) {
const tone = successfulRunHandoffActivityTone(action);
const isWarning = action !== SUCCESSFUL_RUN_HANDOFF_RESOLVED_ACTION;
return (
<div className={cn("space-y-1.5 rounded-lg border px-3 py-2 text-xs", tone.className)}>
<div className="flex items-center gap-1.5">
{isWarning ? <AlertTriangle className={cn("h-3.5 w-3.5 shrink-0", tone.iconClassName)} /> : null}
<Identity name="System" size="sm" />
<span>{formatIssueActivityAction(action)}</span>
<span className="ml-auto shrink-0">{relativeTime(new Date(Date.now() - 3 * 60_000))}</span>
</div>
</div>
);
}
function SuccessfulRunHandoffStates() {
return (
<StoryFrame>
<section className="grid gap-4 lg:grid-cols-[1.15fr_0.85fr]">
<PinnedNoticePanel />
<ActivityEventsPanel />
</section>
<section className="grid gap-4 lg:grid-cols-[0.9fr_1.1fr]">
<IssueCardPanel />
<EscalationCommentPanel />
</section>
</StoryFrame>
);
}
function handoffIssue() {
return createIssue({
id: "issue-handoff",
identifier: "PAP-3053",
issueNumber: 3053,
title: "Add board-visible handoff affordances and activity copy",
status: "in_progress",
priority: "medium",
assigneeAgentId: "agent-codex",
successfulRunHandoff: {
state: "required",
required: true,
sourceRunId: "9cdba892-c7ca-4d93-8604-4843873b127c",
correctiveRunId: "61fdb79b-8012-4676-ac71-2971830e126a",
assigneeAgentId: "agent-codex",
detectedProgressSummary: "Updated the plan and created the first implementation notes.",
createdAt: new Date(),
},
});
}
function StoryFrame({ children, title = "Board-visible handoff states" }: { children: ReactNode; title?: string }) {
return (
<main className="min-h-screen bg-background p-4 text-foreground sm:p-8">
<div className="mx-auto max-w-6xl space-y-5">
<div>
<div className="text-xs font-medium uppercase text-muted-foreground">Successful-run next-step review</div>
<h1 className="mt-1 text-2xl font-semibold">{title}</h1>
</div>
{children}
</div>
</main>
);
}
function PinnedNoticePanel() {
const issue = handoffIssue();
return (
<div className="rounded-lg border border-border bg-card p-4">
<div className="mb-3 text-sm font-medium text-muted-foreground">A. Pinned needs-next-step notice</div>
<IssueBlockedNotice
issueStatus="in_progress"
blockers={[]}
successfulRunHandoff={issue.successfulRunHandoff}
agentName="CodexCoder"
/>
</div>
);
}
function ActivityEventsPanel() {
return (
<div className="rounded-lg border border-border bg-card p-4">
<div className="mb-3 text-sm font-medium text-muted-foreground">B. Activity stream events</div>
<div className="space-y-2">
<ActivityExample action={SUCCESSFUL_RUN_HANDOFF_REQUIRED_ACTION} />
<ActivityExample action="issue.successful_run_handoff_resolved" />
<ActivityExample action={SUCCESSFUL_RUN_HANDOFF_ESCALATED_ACTION} />
</div>
</div>
);
}
function IssueCardPanel() {
const issue = handoffIssue();
return (
<div className="rounded-lg border border-border bg-card p-4">
<div className="mb-3 text-sm font-medium text-muted-foreground">C. Issue card indicator</div>
<KanbanBoard
issues={[
issue,
createIssue({
id: "issue-review",
identifier: "PAP-3054",
issueNumber: 3054,
title: "Review completed next-step recovery",
status: "in_review",
priority: "high",
assigneeAgentId: "agent-cto",
}),
]}
agents={storybookAgents}
onUpdateIssue={() => {}}
/>
</div>
);
}
function EscalationCommentPanel() {
return (
<div className="rounded-lg border border-border bg-card p-4">
<div className="mb-3 text-sm font-medium text-muted-foreground">D. Escalation comment callout</div>
<SuccessfulRunHandoffCommentCallout
text={[
"Paperclip exhausted the bounded successful-run handoff correction for this issue, but it still has no valid issue disposition.",
"",
"This is not a runtime/adapter crash report. The source run succeeded; the remaining problem is the missing `done`, `in_review`, `blocked`, delegated follow-up, or explicit continuation path.",
"",
"- Source issue: [PAP-3053](/PAP/issues/PAP-3053)",
"- Source run: [`9cdba892-c7ca-4d93-8604-4843873b127c`](/PAP/agents/agent-codex/runs/9cdba892-c7ca-4d93-8604-4843873b127c)",
"- Corrective handoff run: [`61fdb79b-8012-4676-ac71-2971830e126a`](/PAP/agents/agent-codex/runs/61fdb79b-8012-4676-ac71-2971830e126a)",
"- Source assignee: [CodexCoder](/PAP/agents/codexcoder)",
"- Latest issue status: `in_progress`",
"- Latest handoff run status: `succeeded`",
"- Normalized cause: `successful_run_missing_state`",
"- Missing disposition: `no_clear_next_step`",
"- Suggested manager action: choose and record a valid issue disposition without copying transcript content.",
"",
"Moving it to `blocked` with an explicit recovery owner so the missing disposition is visible and owned.",
].join("\n")}
/>
</div>
);
}
function SuccessfulRunHandoffPinnedNotice() {
return <StoryFrame title="Pinned needs-next-step notice"><PinnedNoticePanel /></StoryFrame>;
}
function SuccessfulRunHandoffActivityEvents() {
return <StoryFrame title="Activity stream events"><ActivityEventsPanel /></StoryFrame>;
}
function SuccessfulRunHandoffIssueCard() {
return <StoryFrame title="Issue card indicator"><IssueCardPanel /></StoryFrame>;
}
function SuccessfulRunHandoffEscalationComment() {
return <StoryFrame title="Escalation comment callout"><EscalationCommentPanel /></StoryFrame>;
}
const meta = {
title: "Paperclip/Successful Run Handoff",
component: SuccessfulRunHandoffStates,
parameters: {
layout: "fullscreen",
},
} satisfies Meta<typeof SuccessfulRunHandoffStates>;
export default meta;
type Story = StoryObj<typeof meta>;
export const AllStates: Story = {};
export const PinnedNotice: Story = { render: () => <SuccessfulRunHandoffPinnedNotice /> };
export const ActivityEvents: Story = { render: () => <SuccessfulRunHandoffActivityEvents /> };
export const IssueCardIndicator: Story = { render: () => <SuccessfulRunHandoffIssueCard /> };
export const EscalationComment: Story = { render: () => <SuccessfulRunHandoffEscalationComment /> };