paperclip/ui/storybook/stories/assigned-backlog-safeguards.stories.tsx

246 lines
9.3 KiB
TypeScript
Raw Normal View History

Guard assigned backlog liveness (#5428) ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The issue graph and liveness recovery system decide whether assigned work is executable or parked > - Assigned issues created without an explicit status could silently land in backlog, making parents look blocked with no productive wake path > - The server, shared validators, recovery analysis, and UI all need to agree on that execution semantic > - This pull request makes assigned issue creation default to `todo`, flags assigned backlog blockers, and surfaces the state in the board > - The benefit is that parked assigned work becomes intentional and visible instead of creating silent liveness stalls ## What Changed - Adds contract tests for assigned issue creation defaults. - Defaults assigned issue creation to `todo` when status is omitted while preserving explicit `backlog` parking. - Exposes `resolveCreateIssueStatusDefault` through shared validators. - Teaches liveness/blocker attention paths to distinguish assigned backlog blockers. - Adds UI notices, row/header badges, and issue detail safeguards for assigned backlog blockers. - Adds Storybook fixtures and execution-semantics documentation for the assigned-backlog behavior. ## Verification - `pnpm run preflight:workspace-links && pnpm exec vitest run packages/shared/src/validators/issue.test.ts server/src/__tests__/issue-assigned-backlog-contract-routes.test.ts server/src/__tests__/issue-blocker-attention.test.ts server/src/__tests__/issue-liveness.test.ts server/src/__tests__/heartbeat-issue-liveness-escalation.test.ts ui/src/components/IssueAssignedBacklogNotice.test.tsx ui/src/components/IssueRow.test.tsx` — 50 passed, 23 skipped. - Skipped tests were embedded Postgres suites on this host with the repo skip message: `Postgres init script exited with code null. Please check the logs for extra info. The data directory might already exist.` - Pairwise merge check against the issue-controls PR branch completed without conflicts via `git merge --no-commit --no-ff` in a temporary worktree. - Screenshots for assigned-backlog UI states: [light](docs/pr-screenshots/pr-5428/assigned-backlog-light.png), [dark](docs/pr-screenshots/pr-5428/assigned-backlog-dark.png). - Follow-up checks: `pnpm --filter /ui typecheck`; `pnpm --filter /mcp-server build`; `pnpm --filter /mcp-server test`; `pnpm exec vitest run packages/shared/src/validators/issue.test.ts`; focused UI component tests. - Remote PR checks on head `6300b3c`: policy, verify, serialized server shards 1/4-4/4, Canary Dry Run, e2e, Greptile Review, and Snyk all passed. ## Risks - Medium: changes status defaulting for assigned issue creation when the caller omits status. Explicit `backlog` remains supported, and server/shared tests cover both paths. - Medium: liveness classification changes can affect blocker attention labels; focused service and UI tests cover the new assigned-backlog state. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex coding agent, GPT-5 model family (`gpt-5`), tool-enabled Paperclip heartbeat environment. Context window and internal reasoning mode are not exposed by the runtime. ## 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-07 12:25:26 -05:00
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { ReactNode } from "react";
import { CircleDot, Flag, MoreHorizontal, Paperclip } from "lucide-react";
import type { IssueRelationIssueSummary } from "@paperclipai/shared";
import { IssueAssignedBacklogNotice } from "@/components/IssueAssignedBacklogNotice";
import { IssueBlockedNotice } from "@/components/IssueBlockedNotice";
import { IssueRow } from "@/components/IssueRow";
import { storybookAgents, createIssue } from "../fixtures/paperclipData";
const codexAgent = storybookAgents.find((agent) => agent.id === "agent-codex") ?? storybookAgents[0]!;
const qaAgent = storybookAgents.find((agent) => agent.id === "agent-qa") ?? storybookAgents[0]!;
function StoryFrame({ title, children }: { title: string; children: ReactNode }) {
return (
<main className="min-h-screen bg-background p-4 text-foreground sm:p-8">
<div className="mx-auto max-w-5xl space-y-5">
<div>
<div className="text-xs font-medium uppercase text-muted-foreground">Assigned-backlog UI safeguards</div>
<h1 className="mt-1 text-2xl font-semibold">{title}</h1>
</div>
{children}
</div>
</main>
);
}
function CreationFormPanel() {
return (
<div className="rounded-lg border border-border bg-card p-4">
<div className="mb-3 text-sm font-medium text-muted-foreground">A. Issue creation chip bar with intent note</div>
<div className="space-y-3 rounded-md border border-border/60 bg-background p-3">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="w-6 shrink-0 text-center">For</span>
<span className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs">
ClaudeCoder
</span>
<span>in</span>
<span className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs">
Paperclip App
</span>
</div>
<div className="space-y-1.5">
<div className="text-sm font-semibold">Fix flaky deploy step on the worker pipeline</div>
<div className="text-xs text-muted-foreground">
Investigate the intermittent timeout the worker pipeline hit during the last release rehearsal.
</div>
</div>
<div className="flex items-center gap-1.5 border-t border-border pt-3 flex-wrap">
<span className="inline-flex items-center gap-1.5 rounded-md border border-border bg-amber-100/40 px-2 py-1 text-xs dark:bg-amber-500/10">
<CircleDot className="h-3 w-3 text-purple-500" />
Backlog
</span>
<span className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs">
<CircleDot className="h-3 w-3 text-amber-500" />
High
</span>
<span className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground">
<Paperclip className="h-3 w-3" />
Upload
</span>
<span className="inline-flex items-center justify-center rounded-md border border-border p-1 text-xs text-muted-foreground">
<MoreHorizontal className="h-3 w-3" />
</span>
</div>
<div className="flex items-start gap-2 rounded-md border border-amber-300/70 bg-amber-50/90 px-3 py-2 text-xs text-amber-900 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-100">
<Flag className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-600 dark:text-amber-300" />
<span className="leading-snug">
Assigning implies executable intent leave status as <span className="font-medium">Backlog</span> only to deliberately park this. The assignee will not be woken until status moves to <span className="font-medium">Todo</span> or <span className="font-medium">In Progress</span>.
</span>
</div>
</div>
<div className="mt-4 rounded-md border border-border bg-popover p-1 text-xs">
<div className="px-2 pb-1 text-[10px] uppercase text-muted-foreground">Status options</div>
<div className="flex w-full items-start gap-2 rounded px-2 py-1.5 hover:bg-accent/50">
<CircleDot className="h-3 w-3 mt-0.5 shrink-0 text-purple-500" />
<span className="flex flex-col text-left leading-tight">
<span>Backlog</span>
<span className="text-[10px] text-muted-foreground">Parked assignee will not be woken</span>
</span>
</div>
<div className="flex w-full items-start gap-2 rounded bg-accent px-2 py-1.5">
<CircleDot className="h-3 w-3 mt-0.5 shrink-0 text-blue-500" />
<span className="flex flex-col text-left leading-tight">
<span>Todo</span>
<span className="text-[10px] text-muted-foreground">Executable assignee will be woken</span>
</span>
</div>
</div>
</div>
);
}
function AssignedBacklogNoticePanel() {
return (
<div className="rounded-lg border border-border bg-card p-4">
<div className="mb-3 text-sm font-medium text-muted-foreground">B. Issue panel banner parked with assignee</div>
<IssueAssignedBacklogNotice
issueStatus="backlog"
assigneeAgent={qaAgent}
assigneeUserId={null}
onResume={() => undefined}
/>
</div>
);
}
function BlockedByParkedWorkPanel() {
const parkedBlocker: IssueRelationIssueSummary = {
id: "blocker-parked",
identifier: "PAP-3683",
title: "Adapter restart fails after upgrade",
status: "backlog",
priority: "critical",
assigneeAgentId: codexAgent.id,
assigneeUserId: null,
};
return (
<div className="rounded-lg border border-border bg-card p-4">
<div className="mb-3 text-sm font-medium text-muted-foreground">C. Parent issue blocked by parked work</div>
<IssueBlockedNotice
issueStatus="blocked"
blockers={[parkedBlocker]}
blockerAttention={{
state: "needs_attention",
reason: "attention_required",
unresolvedBlockerCount: 1,
coveredBlockerCount: 0,
stalledBlockerCount: 0,
attentionBlockerCount: 1,
sampleBlockerIdentifier: parkedBlocker.identifier,
sampleStalledBlockerIdentifier: null,
}}
/>
</div>
);
}
function ListRowsPanel() {
return (
<div className="rounded-lg border border-border bg-card p-4">
<div className="mb-3 text-sm font-medium text-muted-foreground">D. Issue list row indicators</div>
<div className="rounded-md border border-border">
<IssueRow
issue={createIssue({
id: "issue-blocked-parent",
identifier: "PAP-3643",
issueNumber: 3643,
title: "Restart deploy run after fixed adapter",
status: "blocked",
priority: "high",
blockedBy: [
{
id: "blocker-parked-leaf",
identifier: "PAP-3683",
title: "Adapter restart fails after upgrade",
status: "backlog",
priority: "critical",
assigneeAgentId: codexAgent.id,
assigneeUserId: null,
},
],
blockerAttention: {
state: "needs_attention",
reason: "attention_required",
unresolvedBlockerCount: 1,
coveredBlockerCount: 0,
stalledBlockerCount: 0,
attentionBlockerCount: 1,
sampleBlockerIdentifier: "PAP-3683",
sampleStalledBlockerIdentifier: null,
},
})}
/>
<IssueRow
issue={createIssue({
id: "issue-healthy",
identifier: "PAP-3644",
issueNumber: 3644,
title: "Compute new deploy budget for next cycle",
status: "in_progress",
priority: "medium",
blockedBy: [],
})}
/>
</div>
</div>
);
}
function AllStates() {
return (
<StoryFrame title="Assigned-backlog liveness UI">
<section className="grid gap-4 lg:grid-cols-[1fr_1fr]">
<CreationFormPanel />
<AssignedBacklogNoticePanel />
</section>
<section className="grid gap-4 lg:grid-cols-[1fr_1fr]">
<BlockedByParkedWorkPanel />
<ListRowsPanel />
</section>
</StoryFrame>
);
}
const meta = {
title: "Paperclip/Assigned Backlog Safeguards",
component: AllStates,
parameters: { layout: "fullscreen" },
} satisfies Meta<typeof AllStates>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Overview: Story = {};
export const CreationForm: Story = {
render: () => (
<StoryFrame title="Issue creation chip bar with intent note">
<CreationFormPanel />
</StoryFrame>
),
};
export const AssignedBacklogBanner: Story = {
render: () => (
<StoryFrame title="Issue panel banner — parked with assignee">
<AssignedBacklogNoticePanel />
</StoryFrame>
),
};
export const BlockedByParkedWork: Story = {
render: () => (
<StoryFrame title="Parent issue blocked by parked work">
<BlockedByParkedWorkPanel />
</StoryFrame>
),
};
export const ListRows: Story = {
render: () => (
<StoryFrame title="Issue list row indicators">
<ListRowsPanel />
</StoryFrame>
),
};