mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-20 04:20:38 +09:00
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>
This commit is contained in:
parent
50db8c01d2
commit
454edfe81e
70 changed files with 21919 additions and 125 deletions
|
|
@ -84,6 +84,7 @@ function ScenarioCard({
|
|||
|
||||
function createComment(overrides: Partial<StoryComment>): StoryComment {
|
||||
const createdAt = overrides.createdAt ?? new Date("2026-04-20T14:00:00.000Z");
|
||||
const authorAgentId = overrides.authorAgentId ?? null;
|
||||
return {
|
||||
id: "comment-default",
|
||||
companyId,
|
||||
|
|
@ -91,6 +92,9 @@ function createComment(overrides: Partial<StoryComment>): StoryComment {
|
|||
authorAgentId: null,
|
||||
authorUserId: currentUserId,
|
||||
body: "",
|
||||
authorType: authorAgentId ? "agent" : "user",
|
||||
presentation: null,
|
||||
metadata: null,
|
||||
createdAt,
|
||||
updatedAt: overrides.updatedAt ?? createdAt,
|
||||
...overrides,
|
||||
|
|
@ -384,6 +388,42 @@ const issueChatComments: IssueChatComment[] = [
|
|||
runId: "run-issue-chat-01",
|
||||
runAgentId: codexAgent.id,
|
||||
}),
|
||||
createComment({
|
||||
id: "comment-issue-system-warning",
|
||||
authorType: "system",
|
||||
authorAgentId: null,
|
||||
authorUserId: null,
|
||||
runId: "run-issue-chat-01",
|
||||
runAgentId: codexAgent.id,
|
||||
body: "Paperclip needs a disposition before this issue can continue.",
|
||||
presentation: {
|
||||
kind: "system_notice",
|
||||
tone: "warning",
|
||||
title: "Missing issue disposition",
|
||||
detailsDefaultOpen: false,
|
||||
},
|
||||
metadata: {
|
||||
version: 1,
|
||||
sections: [
|
||||
{
|
||||
title: "Required action",
|
||||
rows: [
|
||||
{ type: "issue_link", label: "Source issue", issueId: issueId, identifier: "PAP-3440", title: "Successful run handoff" },
|
||||
{ type: "agent_link", label: "Assignee", agentId: codexAgent.id, name: codexAgent.name },
|
||||
{ type: "key_value", label: "Status before", value: "in_progress" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Run evidence",
|
||||
rows: [
|
||||
{ type: "run_link", label: "Successful run", runId: "run-issue-chat-01", title: "succeeded" },
|
||||
{ type: "key_value", label: "Normalized cause", value: "Run completed without disposition" },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
createdAt: new Date("2026-04-20T13:54:00.000Z"),
|
||||
}),
|
||||
createComment({
|
||||
id: "comment-issue-queued",
|
||||
body: "@QAChecker please do a quick visual pass after the Storybook build is green.",
|
||||
|
|
|
|||
196
ui/storybook/stories/successful-run-handoff.stories.tsx
Normal file
196
ui/storybook/stories/successful-run-handoff.stories.tsx
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
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 /> };
|
||||
|
|
@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
|||
import { IssueChatUxLab } from "@/pages/IssueChatUxLab";
|
||||
import { InviteUxLab } from "@/pages/InviteUxLab";
|
||||
import { RunTranscriptUxLab } from "@/pages/RunTranscriptUxLab";
|
||||
import { SystemNoticeUxLab } from "@/pages/SystemNoticeUxLab";
|
||||
|
||||
function StoryFrame({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
|
@ -61,6 +62,23 @@ export const RunTranscriptFixtures: Story = {
|
|||
},
|
||||
};
|
||||
|
||||
export const SystemNoticeTreatment: Story = {
|
||||
name: "System Notice Treatment",
|
||||
render: () => (
|
||||
<StoryFrame>
|
||||
<SystemNoticeUxLab />
|
||||
</StoryFrame>
|
||||
),
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Renders the first-class system notice (PAP-3525 plan): warning, danger, and neutral tones in collapsed and expanded states, an in-thread hierarchy comparison against user and agent bubbles, and a before/after replacement of the current nested user-bubble + warning-callout pattern.",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const InviteAndAccessFlow: Story = {
|
||||
name: "Invite And Access Flow",
|
||||
render: () => (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue