paperclip/ui/storybook/stories/assigned-backlog-safeguards.stories.tsx
Dotta e400315cbf
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

245 lines
9.3 KiB
TypeScript

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>
),
};